diff --git a/COMMITTERS b/COMMITTERS
new file mode 100644
index 00000000..85901a07
--- /dev/null
+++ b/COMMITTERS
@@ -0,0 +1,33 @@
+The following people have commit access to the Sakai Project fork
+of the Sparse Map Content System sources originally authored
+by Ian Boston (ian@tfd.co.uk). Note that this is not a
+full list of the authors; for that, you will need to look
+over the log messages to see all the patch contributors.
+
+Committers:
+
+ carl@hallwaytech.com Carl Hall
+ stuart.freeman@et.gatech.edu D. Stuart Freeman
+ lance@rsmart.com Lance Speelmon
+ zach@aeroplanesoftware.com Zach Thomas
+ chris@media.berkeley.edu Chris Tweney
+ arwhyte@umich.edu Anthony Whyte
+
+Contributors:
+
+For a complete list of contributions please see the commit log.
+Contributors include:
+
+ ian@tfd.co.uk Ian Boston (original author)
+ ray@media.berkeley.edu Ray Davis
+ cdunstall@csu.edu.au Chris Dunstall
+ erik.froese@gmail.com Erik Froese
+ duffy@rsmart.com Duffy Gillman
+ johnk@media.berkeley.edu John King
+ kotwal.aadish@gmail.com Aadish Kotwal
+ droma@csu.edu.au Dave Roma
+ mark@dishevelled.net Mark Triggs
+ mawalsh@csu.edu.au Mark Walsh
+ roberttdev@gmail.com Rob Williams
+
+
diff --git a/CONTRIBUTING b/CONTRIBUTING
new file mode 100644
index 00000000..dcc9a57d
--- /dev/null
+++ b/CONTRIBUTING
@@ -0,0 +1,9 @@
+Please read the README and NOTICE files before contributing.
+Contributions are very welcome under those terms, but its your responsibility
+to ensure that the contributions meet those requirements.
+
+All patches prior to this file appearing in the code base were contributed under no
+explicit policy on accepting patches and Timefields Ltd holds no copyright to those
+contributons and makes no assertions as to the IPR status or patent status of those
+contributions.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 00000000..75b52484
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ 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/NOTICE b/NOTICE
new file mode 100644
index 00000000..9ccf1b96
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,18 @@
+Sparse Content Bundle
+Copyright 2011 Timefields Ltd
+
+The Copyright of patches appearing in the code base prior to the appearance of this NOTICE and CONTRIBUTING file
+were accepted under the assumption that re-licensing of those patches, under the Apache 2 Software License to the
+Sakai Foundation was acceptable to the contributors, and that there was nothing in the patches that would prevent
+that from happening, infringe on any patents or IPR. If you are concerned about this, you can find authors using
+the version control system.
+
+-----------------------------------------------------------
+
+This product includes software from the The Apache Software Foundation (http://www.apache.org/).
+
+Patches and contributions made to this code base are made under the terms of the Apache 2 Software License (see para 5).
+
+Binary distributions of this product contain jars developed and licensed by other third parties, identified by the
+LICENSE and NOTICE files included within each jar under the META-INF directory.
+
diff --git a/README.textile b/README.textile
index 087225d1..fed5a961 100644
--- a/README.textile
+++ b/README.textile
@@ -16,21 +16,18 @@ abstract level, but is making a positive effort and selfish effort to only provi
The Implementation works on manipulating sparse objects in the Map with operations like get, insert and delete, but
has no understanding of the underlying implementation of the storage mechanism.
- At the moment we have 2 storage mechanisms implemented, In Memory using a HashMap, and Cassandra. The approach should
-work on any Column Store (Dynamo, BigTable, Riak, Voldomort, Hbase etc) and can also work on RDBMS's including sharded storage.
+ At the moment we have 3 storage mechanisms implemented, In Memory using a HashMap, Cassandra and JDBC capable of doing sharded storage, The approach should
+work on any Column Store (Dynamo, BigTable, Riak, Voldomort, Hbase etc).
- At the moment there is no query support, expecting all access to be via column IDs, and multiple views to be written to the
-underlying store.
-
- The intention is to provide write through caches based on EhCache or Infinispan.
+ Query support is provided by finder messages that use index table written on update.
+
+ Caching support is via an interface allowing external providers. In Nakamura there is an EhCache implementation and it would be a relatively simple task to write an Infinispan version.
Transactions are supported, if supported by the underlying implementation of the storage, otherwise all operations are BASIC, non Atomic and immediate in nature.
-We will add search indexes at some point using Lucene, perhaps in the form of Zoie
-
-
- At this stage its pre-alpha, untested for performance and scalability and incomplete.
+ Search is provided in the form of a companion project that uses SolrJ 4
+ The JDBC Driver has configuration files for Derby, MySQL, Oracle, PostgreSQL.
h2. Backlog
@@ -116,3 +113,17 @@ Throughput is users added per second.
So far it looks like the code is concurrent, but MySQL is considerably slower than Cassandra or Memory. Below the Fighting for cores
the box doesn't have enough CPUs to support the DB if present and the code.
+
+
+h1. Contributions, Patches, and License.
+
+The code in this code base is (c) Timefields Ltd and licensed to the Sakai Foundation under a Apache 2 Software License. Before making
+a contribution by means of a patch or otherwise please ensure that you read and understand the terms under which a patch or contribution
+will be accepted, outlined in the NOTICES file. All patches and contributions are made under those terms with no exceptions.
+
+I am sorry if this sounds a bit legal, but I want to be able to always license this software to the Sakai Foundation under an Apache 2 license
+and so I have to insist that no contributions are made that would prevent that from happening. I cani't ask everyone who submits a patch to sign a
+legal document, so this is the next best thing. If you have a problem with this approach, please email me and we can try and work it out.
+
+BTW, IANAL :)
+
diff --git a/core/pom.xml b/core/pom.xml
new file mode 100644
index 00000000..de2b11c6
--- /dev/null
+++ b/core/pom.xml
@@ -0,0 +1,278 @@
+
+
+ 4.0.0
+
+ org.sakaiproject.nakamura
+ core-base
+ 5-SNAPSHOT
+ ../pom.xml
+
+ org.sakaiproject.nakamura
+ org.sakaiproject.nakamura.core
+ bundle
+ 1.5.1-SNAPSHOT
+ Sparse Map :: Sparse Map Content Storage bundle.
+ Server that uses a sparse map to represent content mapping closely to a colum database like Cassandra.
+
+ UTF-8
+ true
+
+
+ scm:git:git://github.com/sakaiproject/sparsemapcontent.git
+ scm:git:git@github.com:sakaiproject/sparsemapcontent.git
+ http://github.com/sakaiproject/sparsemapcontent/
+
+
+
+
+ org.apache.felix
+ maven-scr-plugin
+
+
+ generate-scr-scrdescriptor
+
+ scr
+
+
+ core-serviceComponents.xml
+
+
+
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ true
+
+
+ sparse-map
+ ${project.artifactId}
+
+ org.sakaiproject.nakamura.api.lite.*
+
+
+ *
+
+
+ org.apache.commons.io; version="1.4",
+ com.google.common.collect; version="9.0.0",
+ *
+
+ org.sakaiproject.nakamura.lite.*
+
+
+
+
+ maven-resources-plugin
+ 2.5
+
+
+ copy-osgi-resources
+ prepare-package
+
+ copy-resources
+
+
+ ${basedir}/target/classes
+
+
+ ${basedir}/target/scr-plugin-generated
+ true
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 2.3.2
+
+
+
+ test-jar
+
+ test-jar
+
+
+
+
+
+ ${basedir}/target/classes/META-INF/MANIFEST.MF
+
+ OSGI-INF/core-serviceComponents.xml,OSGI-INF/serviceComponents.xml
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.5
+
+
+ **/Test*.java
+ **/*Test.java
+ **/*TestCase.java
+
+
+
+
+
+
+
+
+ org.eclipse.m2e
+ lifecycle-mapping
+ 1.0.0
+
+
+
+
+
+ org.apache.felix
+ maven-scr-plugin
+ [1.0.0,)
+ scr
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+ [2.3.1,)
+ install-file
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ [1.0.0,)
+ enforce
+
+
+
+
+
+
+
+
+
+
+
+
+ javax.servlet
+ servlet-api
+ 2.4
+
+
+ commons-pool
+ commons-pool
+ 1.5
+
+
+ commons-lang
+ commons-lang
+ 2.5
+
+
+ commons-io
+ commons-io
+ 1.4
+
+
+ commons-codec
+ commons-codec
+ 1.4
+
+
+ com.googlecode.guava-osgi
+ guava-osgi
+ 9.0.0
+
+
+
+ org.apache.felix
+ org.osgi.core
+ 1.2.0
+ provided
+
+
+ org.apache.felix
+ org.osgi.compendium
+ 1.2.0
+ provided
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.5.10
+
+
+ org.slf4j
+ slf4j-simple
+ 1.5.10
+ test
+
+
+ org.mockito
+ mockito-all
+ 1.8.5
+
+
+
+
+ org.apache.felix
+ org.apache.felix.scr.annotations
+
+
+ org.apache.derby
+ derby
+ 10.6.2.1
+ test
+
+
+ junit
+ junit
+ 4.4
+ test
+
+
+ findbugs
+ annotations
+ 1.0.0
+ provided
+
+
+
+
+
+
+
+ sakai-maven2
+ Sakai Maven Repo
+ default
+ http://source.sakaiproject.org/maven2
+
+ true
+ ignore
+
+
+ false
+ ignore
+
+
+
+
+
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/BaseColumnFamilyCacheManager.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/BaseColumnFamilyCacheManager.java
new file mode 100644
index 00000000..b57a7dd5
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/BaseColumnFamilyCacheManager.java
@@ -0,0 +1,43 @@
+package org.sakaiproject.nakamura.api.lite;
+
+import java.util.Map;
+
+public abstract class BaseColumnFamilyCacheManager implements ColumnFamilyCacheManager {
+
+ public Map getAccessControlCache() {
+ throw new UnsupportedOperationException("Use getCache(String columnFamily)");
+ }
+
+ public Map getAuthorizableCache() {
+ throw new UnsupportedOperationException("Use getCache(String columnFamily)");
+ }
+
+ public Map getContentCache() {
+ throw new UnsupportedOperationException("Use getCache(String columnFamily)");
+ }
+
+ /**
+ * This method deals with backward compatibility of StorageCacheManager which was developed when
+ * @param configuration
+ * @param columnFamily
+ * @param storageCacheManager
+ * @return
+ */
+ public static Map getCache(Configuration configuration, String columnFamily,
+ StorageCacheManager storageCacheManager) {
+ if ( storageCacheManager instanceof ColumnFamilyCacheManager ) {
+ return ((ColumnFamilyCacheManager) storageCacheManager).getCache(columnFamily);
+ }
+ if ( configuration.getAclColumnFamily().equals(columnFamily)) {
+ return storageCacheManager.getAccessControlCache();
+ }
+ if ( configuration.getAuthorizableColumnFamily().equals(columnFamily)) {
+ return storageCacheManager.getAuthorizableCache();
+ }
+ if ( configuration.getContentColumnFamily().equals(columnFamily)) {
+ return storageCacheManager.getContentCache();
+ }
+ return null;
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/CacheHolder.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/CacheHolder.java
new file mode 100644
index 00000000..cfcf0fb7
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/CacheHolder.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite;
+
+import java.util.Map;
+
+public class CacheHolder {
+
+ private Map o;
+ private long locker;
+ private long ttl;
+
+ public CacheHolder(Map o) {
+ this.o = o;
+ this.ttl = System.currentTimeMillis()+10000L;
+ this.locker = -1;
+ }
+ public CacheHolder(Map o, long locker) {
+ this.o = o;
+ this.ttl = System.currentTimeMillis()+10000L;
+ this.locker = locker;
+ }
+
+ public Map get() {
+ return o;
+ }
+
+ public boolean isLocked(long managerId) {
+ if ( locker == -1 || managerId == locker ) {
+ return false;
+ }
+ return (System.currentTimeMillis() < ttl);
+ }
+ public boolean wasLockedTo(long managerId) {
+ return (locker == managerId);
+ }
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/ClientPoolException.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/ClientPoolException.java
similarity index 100%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/ClientPoolException.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/ClientPoolException.java
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/ColumnFamilyCacheManager.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/ColumnFamilyCacheManager.java
new file mode 100644
index 00000000..1134cd18
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/ColumnFamilyCacheManager.java
@@ -0,0 +1,10 @@
+package org.sakaiproject.nakamura.api.lite;
+
+import java.util.Map;
+
+public interface ColumnFamilyCacheManager extends StorageCacheManager {
+
+ public Map getCache(String columnFamily);
+
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/CommitHandler.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/CommitHandler.java
new file mode 100644
index 00000000..e0c344a6
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/CommitHandler.java
@@ -0,0 +1,12 @@
+package org.sakaiproject.nakamura.api.lite;
+
+/**
+ * Performs a commit.
+ * @author ieb
+ *
+ */
+public interface CommitHandler {
+
+ void commit();
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/Configuration.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/Configuration.java
similarity index 80%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/Configuration.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/Configuration.java
index 1d3c0522..f57193e8 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/Configuration.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/Configuration.java
@@ -18,6 +18,8 @@
package org.sakaiproject.nakamura.api.lite;
+import java.util.Map;
+
/**
* An Interface to define configuration for the sparse content store.
*/
@@ -50,4 +52,25 @@ public interface Configuration {
*/
String getContentColumnFamily();
+ /**
+ * @return name of the lock column family.
+ */
+ String getLockColumnFamily();
+
+ /**
+ * @return the config, shared by all drivers.
+ */
+ Map getSharedConfig();
+
+ /**
+ * @return an array of properties names that should be indexed.
+ */
+ String[] getIndexColumnNames();
+
+ /**
+ *
+ * @return an array of index column types
+ */
+ String[] getIndexColumnTypes();
+
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/DataFormatException.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/DataFormatException.java
new file mode 100644
index 00000000..ebce9904
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/DataFormatException.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite;
+
+/**
+ * A more specialized error for StorageClients when
+ * they are asked to store malformed data.
+ *
+ * For example, this exception will be thrown if the
+ * data to store is too large.
+ */
+public class DataFormatException extends StorageClientException {
+
+ private static final long serialVersionUID = 1464691562897983604L;
+
+ public DataFormatException(String message, Throwable t) {
+ super(message, t);
+ }
+
+ public DataFormatException(String message) {
+ super(message);
+ }
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/Feedback.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/Feedback.java
new file mode 100644
index 00000000..530deddc
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/Feedback.java
@@ -0,0 +1,16 @@
+package org.sakaiproject.nakamura.api.lite;
+
+import java.io.File;
+
+
+public interface Feedback {
+
+ void log(String format, Object ... params);
+
+ void exception(Throwable e);
+
+ void newLogFile(File currentFile);
+
+ void progress(boolean dryRun, long done, long toDo);
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/MigrateContentService.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/MigrateContentService.java
new file mode 100644
index 00000000..f8da7710
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/MigrateContentService.java
@@ -0,0 +1,30 @@
+package org.sakaiproject.nakamura.api.lite;
+
+import java.io.IOException;
+
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
+
+public interface MigrateContentService {
+
+ /**
+ * @param dryRun
+ * dry run the migration
+ * @param limit
+ * if dry running, limit the number
+ * @param reindexAll
+ * if try reindex all
+ * @param feedback
+ * a logger to provide feedback to. If you want to control the
+ * Migration, implement your own logger and throw a
+ * RuntimeException from the info or debug methods to stop the
+ * migrator in the case of an emergency.
+ * @throws ClientPoolException
+ * @throws StorageClientException
+ * @throws AccessDeniedException
+ * @throws IOException
+ * @throws PropertyMigrationException thrown if there are unresolved dependencies.
+ */
+ void migrate(boolean dryRun, int limit, boolean reindexAll, Feedback feedback)
+ throws ClientPoolException, StorageClientException, AccessDeniedException, IOException, PropertyMigrationException;
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/MigrationService.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/MigrationService.java
new file mode 100644
index 00000000..c2a74adc
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/MigrationService.java
@@ -0,0 +1,31 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite;
+
+@SuppressWarnings({"UnusedDeclaration"})
+public interface MigrationService {
+
+ /**
+ * Perform upgrades by running the upgrade() methods of all registered PropertyMigrator instances.
+ * @param dryRun True if you want to run the upgrade without actually changing data; false if you want data changes saved.
+ * @param verify True if you want to check upgraded data using the PropertyMigrator.verify() method.
+ * @throws Exception if an unrecoverable error occurred.
+ */
+ void doMigration(boolean dryRun, boolean verify) throws Exception;
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/PropertyMigrationException.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/PropertyMigrationException.java
new file mode 100644
index 00000000..1fcf8a09
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/PropertyMigrationException.java
@@ -0,0 +1,14 @@
+package org.sakaiproject.nakamura.api.lite;
+
+public class PropertyMigrationException extends Exception {
+
+ public PropertyMigrationException(String message) {
+ super(message);
+ }
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -3856860605825678993L;
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/PropertyMigrator.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/PropertyMigrator.java
new file mode 100644
index 00000000..fc3ee0b0
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/PropertyMigrator.java
@@ -0,0 +1,80 @@
+package org.sakaiproject.nakamura.api.lite;
+
+import java.util.Map;
+
+/**
+ * Implementations of PropertyMigrators registered with OSGi are called by the
+ * MigrateContentComponent, when its activated (normally disabled). All
+ * registered implementation will be called, once for each Map within the
+ * system. If they determine that the map is of the appropriate type and needs
+ * modification, they should modify it, and return true. If not they should
+ * leave the map untouched. There is no guarantee in what order each migrator
+ * might be called. The lack of ordering avoids the situation where one migrator
+ * has a dependency on another migrator which would require those in production
+ * to ensure that they had all dependent migrators register. If that becomes a
+ * requirement then we will need to build a mechanism where migrators can
+ * express their dependencies and refuse to run if things they depend on are not
+ * present in the stack..... but perhaps thats what OSGi is for?. If any
+ * PropertyMigrator modifies a set of properties, the map will be re-saved under
+ * the same key. If no properties are modified by any PropertyMigrators, then
+ * the object will be re-indexed with the current index operation. Un-filtered
+ * access is given to all properties, so anyone implementing this interface must
+ * take great care not to break referential integrity of each object or
+ * invalidate the internals of the object.
+ *
+ * The MigrateContentComponent is not active by default, and should only be made
+ * active by an Administrator using the Web UI.
+ *
+ * The migrate methods will be called once for every object within the system.
+ * (could be billions of times).
+ *
+ * @author ieb
+ *
+ */
+public interface PropertyMigrator {
+
+ /**
+ * Option: If set to "true" in the option set then the PropertyMigrator will
+ * only run once, else, the PropertyMigrator will run whenever its present.
+ */
+ public static final String OPTION_RUNONCE = "runonce";
+
+ /**
+ * @param rid
+ * the row id of the current object as loaded from the store. If
+ * the property representing the key for the type of object is
+ * changed, this object will be saved under a new rowid. The
+ * calculation of the rowid depends on the storage implementation
+ * and the value of the key.
+ * @param properties
+ * a map of properties. Implementations are expected to modify
+ * this map, and return true if modifications are made.
+ * @return true if any modifications were made to properties, false
+ * otherwise.
+ */
+ boolean migrate(String rid, Map properties);
+
+ /**
+ * @return get a list of dependencies that this PropertyMigrator is
+ * dependent on. If the named dependencies have not already been run
+ * or are missing from the current set, then the migration will
+ * refuse to run. The value of each element of getDependencies()
+ * should match the value of getName() of the implementation of this
+ * interface on which there is a dependency.
+ */
+ String[] getDependencies();
+
+ /**
+ * @return get the name of this dependency, which is used in
+ * getDependencies(). It must be globally unique over all
+ * implementations of PropertyMigrator. ie getClass().getName() is a
+ * reasonable choice.
+ */
+ String getName();
+
+ /**
+ * @return get a map of options for the migrator.
+ */
+ Map getOptions();
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/storage/Disposable.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/RemoveProperty.java
similarity index 82%
rename from src/main/java/org/sakaiproject/nakamura/lite/storage/Disposable.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/RemoveProperty.java
index 85c446f2..e1e1b82f 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/storage/Disposable.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/RemoveProperty.java
@@ -1,4 +1,4 @@
-/*
+/**
* Licensed to the Sakai Foundation (SF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -15,16 +15,8 @@
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
-package org.sakaiproject.nakamura.lite.storage;
-
-/**
- * Things that are disposable, must be closed.
- *
- * @author ieb
- *
- */
-public interface Disposable {
+package org.sakaiproject.nakamura.api.lite;
- void close();
+public class RemoveProperty {
}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/Repository.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/Repository.java
similarity index 79%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/Repository.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/Repository.java
index eac78a01..8b738018 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/Repository.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/Repository.java
@@ -22,9 +22,17 @@
/**
* Repository container that provides a mechanism to login to the sparse content
* store.
+ * @since 1.0
*/
public interface Repository {
+ /**
+ * The prefix on all system properties in the repository. Anything prefixed
+ * with this is a system proper anything not prefixed with this is not a
+ * system property.
+ */
+ public static final String SYSTEM_PROP_PREFIX = "_";
+
/**
* Login with a user name and password
*
@@ -39,6 +47,7 @@ public interface Repository {
* If there was a problem with the storage pool.
* @throws AccessDeniedException
* If the user was denied access.
+ * @since 1.0
*/
Session login(String username, String password) throws ClientPoolException,
StorageClientException, AccessDeniedException;
@@ -53,6 +62,7 @@ Session login(String username, String password) throws ClientPoolException,
* If there was a problem with the storage pool.
* @throws AccessDeniedException
* If the anon was denied access.
+ * @since 1.0
*/
Session login() throws ClientPoolException, StorageClientException, AccessDeniedException;
@@ -66,6 +76,7 @@ Session login(String username, String password) throws ClientPoolException,
* If there was a problem with the storage pool.
* @throws AccessDeniedException
* If admin was denied access.
+ * @since 1.0
*/
Session loginAdministrative() throws ClientPoolException, StorageClientException,
AccessDeniedException;
@@ -82,8 +93,24 @@ Session loginAdministrative() throws ClientPoolException, StorageClientException
* If there was a problem with the storage pool.
* @throws AccessDeniedException
* If the user was denied access.
+ * @since 1.0
*/
Session loginAdministrative(String username) throws ClientPoolException,
StorageClientException, AccessDeniedException;
+ /**
+ * Perform an administrative login bypassing login enabled checks. Only
+ * internal system operations should use this. Anything related to a login
+ * should never use use.
+ *
+ * @param username
+ * @return
+ * @throws StorageClientException
+ * @throws ClientPoolException
+ * @throws AccessDeniedException
+ * @since 1.4
+ */
+ Session loginAdministrativeBypassEnable(String username) throws StorageClientException,
+ ClientPoolException, AccessDeniedException;
+
}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/Session.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/Session.java
similarity index 84%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/Session.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/Session.java
index ce48646c..963b0a32 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/Session.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/Session.java
@@ -21,6 +21,7 @@
import org.sakaiproject.nakamura.api.lite.accesscontrol.Authenticator;
import org.sakaiproject.nakamura.api.lite.authorizable.AuthorizableManager;
import org.sakaiproject.nakamura.api.lite.content.ContentManager;
+import org.sakaiproject.nakamura.api.lite.lock.LockManager;
/**
* A lightweight container bound to the user that will maintain state associated
@@ -56,6 +57,9 @@ public interface Session {
* @throws StorageClientException
*/
ContentManager getContentManager() throws StorageClientException;
+
+
+ LockManager getLockManager() throws StorageClientException;
/**
* @return the userID that this session is bound to.
@@ -66,4 +70,16 @@ public interface Session {
Repository getRepository();
+ /**
+ * Perform a commit on any pending operations.
+ */
+ void commit();
+
+ /**
+ * Add a commit handler for a certain key. Will replace any other commit handler of the same key.
+ * @param key
+ * @param commitHandler
+ */
+ void addCommitHandler(String key, CommitHandler commitHandler);
+
}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/CacheHolder.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/SessionAdaptable.java
similarity index 78%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/CacheHolder.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/SessionAdaptable.java
index df59b8b5..b4108634 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/CacheHolder.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/SessionAdaptable.java
@@ -1,4 +1,4 @@
-/*
+/**
* Licensed to the Sakai Foundation (SF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -17,18 +17,8 @@
*/
package org.sakaiproject.nakamura.api.lite;
-import java.util.Map;
-
-public class CacheHolder {
-
- private Map o;
-
- public CacheHolder(Map o) {
- this.o = o;
- }
-
- public Map get() {
- return o;
- }
+public interface SessionAdaptable {
+
+ Session getSession();
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/SparseSessionTracker.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/SparseSessionTracker.java
new file mode 100644
index 00000000..0a329e8d
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/SparseSessionTracker.java
@@ -0,0 +1,37 @@
+package org.sakaiproject.nakamura.api.lite;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Tracks sessions and provides a mechanism to retrieve them, based on request
+ * or thread. Retrieval on thread should only be used where its know that the
+ * underlying request processing model will the thread based. If event based
+ * processing is being used, no assumption about thread should be made.
+ *
+ * @author ieb
+ *
+ */
+public interface SparseSessionTracker {
+
+ /**
+ * Register a session against a request.
+ *
+ * @param login
+ * the session to be registered
+ * @param request
+ * the request to register against, if null registration will be
+ * performed on the thread and not on the thread.
+ * @return the session just registered.
+ */
+ Session register(Session login, HttpServletRequest request);
+
+ /**
+ * @param request
+ * the request to get the session from, if null, the thread will
+ * be inspected.
+ * @return the session that was previously registered, or null if no session
+ * was registered.
+ */
+ Session get(HttpServletRequest request);
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageCacheManager.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageCacheManager.java
new file mode 100644
index 00000000..d6c845c2
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageCacheManager.java
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite;
+
+import java.util.Map;
+
+/**
+ * Provides Cache implementations for all the three areas represented as Maps.
+ * If an implementation of this interface is present it will be used.
+ */
+public interface StorageCacheManager {
+
+ /**
+ * @return a Cache, implementing the Map interface, although the keys wont
+ * clash should be a separate memory space from the other caches to
+ * prevent memory poisoning. It would be theoretically possible to
+ * generate a cache ID for some content that could be shared in the
+ * authorizable or access control space, however thats finding a key
+ * matching a pattern that collides with a specific pattern after
+ * both have been hashed with SHA1. The probability or random
+ * collision in SHA1 is 1 in 1E14, so generating a collision for 2
+ * string matching specific patterns is probably far greater than
+ * that.
+ */
+ Map getAccessControlCache();
+
+ /**
+ * @return Should be a separate cache, not sharing the same memory space as
+ * others, see above for why.
+ */
+ Map getAuthorizableCache();
+
+ /**
+ * @return Should be a separate cache, not sharing the same memory space as
+ * others, see above for why.
+ */
+ Map getContentCache();
+
+
+ /**
+ * Get a named cache.
+ * @param cacheName
+ * @return
+ */
+ Map getCache(String cacheName);
+
+
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/StorageClientException.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageClientException.java
similarity index 100%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/StorageClientException.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageClientException.java
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/StorageClientUtils.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageClientUtils.java
similarity index 74%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/StorageClientUtils.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageClientUtils.java
index cf94491b..f35c631d 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/StorageClientUtils.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageClientUtils.java
@@ -21,12 +21,18 @@
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.Maps;
+import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.time.FastDateFormat;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
+import org.sakaiproject.nakamura.api.lite.content.Content;
+import org.sakaiproject.nakamura.api.lite.content.ContentManager;
import org.sakaiproject.nakamura.api.lite.util.Type1UUID;
+import org.sakaiproject.nakamura.lite.storage.spi.types.Types;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.security.MessageDigest;
@@ -56,11 +62,7 @@ public class StorageClientUtils {
* Default hashing algorithm for passwords
*/
public final static String SECURE_HASH_DIGEST = "SHA-512";
- /**
- * Charset for encoding byte data as char
- */
- public static final char[] URL_SAFE_ENCODING = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
- .toCharArray();
+
/**
* Based on JackRabbit: Jackrabbit uses a subset of 8601 (8601:2000) for
* their date times.
@@ -68,7 +70,7 @@ public class StorageClientUtils {
public static String ISO8601_JCR_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSZZ";
@SuppressWarnings("unused")
private final static FastDateFormat ISO8601_JCR_FORMAT = FastDateFormat.getInstance(
- ISO8601_JCR_PATTERN, TimeZone.getTimeZone("UTC"), Locale.ROOT);
+ ISO8601_JCR_PATTERN, TimeZone.getTimeZone("UTC"), Locale.ENGLISH);
private static final Logger LOGGER = LoggerFactory.getLogger(StorageClientUtils.class);
@@ -81,6 +83,8 @@ public class StorageClientUtils {
* @param object
* the storage object
* @return a string representation of the storage object.
+ * @deprecated the application code should convert to a string if necessary,
+ * this is not required for storage any more.
*/
@Deprecated
public static String toString(Object object) {
@@ -123,6 +127,10 @@ public static String getAltField(String field, String streamId) {
* @param object
* the object to place in store.
* @return the Store representation of the object.
+ * @deprecated Objects do not need to be converted when placing in the
+ * store, provided they are one of the ones listed in
+ * {@link Types.ALLTYPES}. If they are not your code should
+ * convert to one or more of those types.
*/
@Deprecated
public static Object toStore(Object object) {
@@ -135,6 +143,9 @@ public static Object toStore(Object object) {
* @param value
* the store object
* @return a byte[] of the store object.
+ * @deprecated if its a byte[] just use it as a byte[] otherwise convert to
+ * a byte[] before storing. eg
+ * String.valueOf(value).getBytes("UTF-8")
*/
@Deprecated
public static byte[] toBytes(Object value) {
@@ -185,6 +196,9 @@ public static String getParentObjectPath(String objectPath) {
* element in the path.
*/
public static String getObjectName(String objectPath) {
+ if ( objectPath == null || "".equals(objectPath)) {
+ return "";
+ }
if ("/".equals(objectPath)) {
return "/";
}
@@ -209,6 +223,15 @@ public static String getObjectName(String objectPath) {
*/
// TODO: Unit test
public static String insecureHash(String naked) {
+ try {
+ return insecureHash(naked.getBytes(UTF8));
+ } catch (UnsupportedEncodingException e3) {
+ LOGGER.error("no UTF-8 Envoding, get a real JVM, nothing will work here. NPE to come");
+ return null;
+ }
+ }
+
+ public static String insecureHash(byte[] b) {
try {
MessageDigest md;
try {
@@ -219,12 +242,11 @@ public static String insecureHash(String naked) {
} catch (NoSuchAlgorithmException e2) {
LOGGER.error("You have no Message Digest Algorightms intalled in this JVM, secure Hashes are not availalbe, encoding bytes :"
+ e2.getMessage());
- return encode(StringUtils.leftPad(naked, 10, '_').getBytes(UTF8),
- URL_SAFE_ENCODING);
+ return encode(StringUtils.leftPad((new String(b,"UTF-8")), 10, '_').getBytes(UTF8));
}
}
- byte[] bytes = md.digest(naked.getBytes(UTF8));
- return encode(bytes, URL_SAFE_ENCODING);
+ byte[] bytes = md.digest(b);
+ return encode(bytes);
} catch (UnsupportedEncodingException e3) {
LOGGER.error("no UTF-8 Envoding, get a real JVM, nothing will work here. NPE to come");
return null;
@@ -250,13 +272,12 @@ public static String secureHash(String password) {
} catch (NoSuchAlgorithmException e2) {
LOGGER.error("You have no Message Digest Algorightms intalled in this JVM, secure Hashes are not availalbe, encoding bytes :"
+ e2.getMessage());
- return encode(StringUtils.leftPad(password, 10, '_').getBytes(UTF8),
- URL_SAFE_ENCODING);
+ return encode(StringUtils.leftPad(password, 10, '_').getBytes(UTF8));
}
}
}
byte[] bytes = md.digest(password.getBytes(UTF8));
- return encode(bytes, URL_SAFE_ENCODING);
+ return encode(bytes);
} catch (UnsupportedEncodingException e3) {
LOGGER.error("no UTF-8 Envoding, get a real JVM, nothing will work here. NPE to come");
return null;
@@ -273,46 +294,31 @@ public static String secureHash(String password) {
* the shorter it is the longer the result. Dont be dumb and use
* an encoding size of < 2.
* @return
+ * @deprecated use encode(byte[])
*/
+ @Deprecated
public static String encode(byte[] hash, char[] encode) {
- StringBuilder sb = new StringBuilder((hash.length * 15) / 10);
- int x = (int) (hash[0] + 128);
- int xt = 0;
- int i = 0;
- while (i < hash.length) {
- if (x < encode.length) {
- i++;
- if (i < hash.length) {
- if (x == 0) {
- x = (int) (hash[i] + 128);
- } else {
- x = (x + 1) * (int) (hash[i] + 128);
- }
- } else {
- sb.append(encode[x]);
- break;
- }
- }
- xt = x % encode.length;
- x = x / encode.length;
- sb.append(encode[xt]);
- }
-
- return sb.toString();
+ return encode(hash);
+ }
+
+ public static String encode(byte[] hash) {
+ return Base64.encodeBase64URLSafeString(hash);
}
/**
* Converts to an Immutable map, with keys that are in the filter not
- * transdered. Nested maps are also transfered.
+ * transfered. Nested maps are also transfered.
*
- * @param
- * @param
- * @param source
- * @param filter
- * @return
+ * @param the type of the key
+ * @param the type of the value
+ * @param source a map of values to start with
+ * @param modified a map to oveeride values in source
+ * @param include if not null, only include these keys in the returned map
+ * @param exclude if not null, exclude these keys from the returned map
+ * @return a map with the modifications applied and filtered by the includes and excludes
*/
@SuppressWarnings("unchecked")
- public static Map getFilterMap(Map source, Map modified, Set include, Set exclude) {
+ public static Map getFilterMap(Map source, Map modified, Set include, Set exclude, boolean includingRemoveProperties ) {
if ((modified == null || modified.size() == 0) && (include == null) && ( exclude == null || exclude.size() == 0)) {
if ( source instanceof ImmutableMap ) {
return source;
@@ -320,6 +326,7 @@ public static Map getFilterMap(Map source, Map modified
return ImmutableMap.copyOf(source);
}
}
+
Builder filteredMap = new ImmutableMap.Builder();
for (Entry e : source.entrySet()) {
K k = e.getKey();
@@ -329,7 +336,9 @@ public static Map getFilterMap(Map source, Map modified
V o = modified.get(k);
if (o instanceof Map) {
filteredMap.put(k,
- (V) getFilterMap((Map) o, null, null, exclude));
+ (V) getFilterMap((Map) o, null, null, exclude, includingRemoveProperties));
+ } else if ( includingRemoveProperties ) {
+ filteredMap.put(k, o);
} else if ( !(o instanceof RemoveProperty) ) {
filteredMap.put(k, o);
}
@@ -337,7 +346,7 @@ public static Map getFilterMap(Map source, Map modified
Object o = e.getValue();
if (o instanceof Map) {
filteredMap.put(k,
- (V) getFilterMap((Map) e.getValue(), null, null, exclude));
+ (V) getFilterMap((Map) e.getValue(), null, null, exclude, includingRemoveProperties));
} else {
filteredMap.put(k, e.getValue());
}
@@ -365,9 +374,9 @@ public static Map getFilterMap(Map source, Map modified
* over depth of nesting. Keys in the filter set are not transfered
* Resulting map is mutable.
*
- * @param source
- * @param filter
- * @return
+ * @param source a map of values to modify
+ * @param filter a map of values to remove by key from source
+ * @return the map less any keys from filter
*/
@SuppressWarnings("unchecked")
public static Map getFilteredAndEcodedMap(Map source,
@@ -391,7 +400,7 @@ public static Map getFilteredAndEcodedMap(Map so
* @return a UUID, compact encoded, suitable for use in URLs
*/
public static String getUuid() {
- return StorageClientUtils.encode(Type1UUID.next(), StorageClientUtils.URL_SAFE_ENCODING);
+ return StorageClientUtils.encode(Type1UUID.next());
}
/**
@@ -424,6 +433,7 @@ public static long toLong(Object object) {
* @param object
* @return the store object as a {@link Calendar}
* @throws ParseException
+ * @deprecated no need to convert, just get the calendar object directly out of the store.
*/
@Deprecated
public static Calendar toCalendar(Object object) throws ParseException {
@@ -432,7 +442,7 @@ public static Calendar toCalendar(Object object) throws ParseException {
} else if (object == null || object instanceof RemoveProperty) {
return null;
}
- final SimpleDateFormat sdf = new SimpleDateFormat(ISO8601_JCR_PATTERN, Locale.ROOT);
+ final SimpleDateFormat sdf = new SimpleDateFormat(ISO8601_JCR_PATTERN, Locale.ENGLISH);
final Date date = sdf.parse(toString(object));
final Calendar c = Calendar.getInstance();
c.setTime(date);
@@ -469,7 +479,22 @@ public static String newPath(String path, String child) {
*/
@SuppressWarnings("unchecked")
public static T getSetting(Object setting, T defaultValue) {
- if (setting != null) {
+ if (setting != null && defaultValue != null) {
+ if (defaultValue.getClass().isAssignableFrom(setting.getClass())) {
+ return (T) setting;
+ }
+ // handle conversions
+ if ( defaultValue instanceof Long ) {
+ return (T) new Long(String.valueOf(setting));
+ } else if ( defaultValue instanceof Integer ) {
+ return (T) new Integer(String.valueOf(setting));
+ } else if (defaultValue instanceof Boolean ) {
+ return (T) Boolean.valueOf(String.valueOf(setting));
+ } else if ( defaultValue instanceof Double ) {
+ return (T) new Double(String.valueOf(setting));
+ } else if ( defaultValue instanceof String[] ) {
+ return (T) StringUtils.split(String.valueOf(setting), ',');
+ }
return (T) setting;
}
return defaultValue;
@@ -516,8 +541,8 @@ public static String arrayUnEscape(String string) {
/**
* @param object
* @return null or the store object converted to a string[]
+ * @deprecated no need to convert, just get the String[] object directly out of the store.
*/
- // TODO: Unit test
@Deprecated
public static String[] toStringArray(Object object) {
if ( object instanceof String[] ) {
@@ -535,10 +560,10 @@ public static String[] toStringArray(Object object) {
/**
* @param object
- * @return null or the store object converted to a string[]
+ * @return null or the store object converted to a Calendar[]
* @throws ParseException
+ * @deprecated no need to convert, just get the Calendar[] object directly out of the store.
*/
- // TODO: Unit test
@Deprecated
public static Calendar[] toCalendarArray(Object object) throws ParseException {
if ( object instanceof Calendar[] ) {
@@ -592,6 +617,14 @@ public static Session adaptToSession(Object source) {
}
}
+ /**
+ * Make the method on the target object accessible and then invoke it.
+ * @param target the object with the method to invoke
+ * @param methodName the name of the method to invoke
+ * @param args the arguments to pass to the invoked method
+ * @param argsTypes the types of the arguments being passed to the method
+ * @return
+ */
private static Object safeMethod(Object target, String methodName, Object[] args,
@SuppressWarnings("rawtypes") Class[] argsTypes) {
if (target != null) {
@@ -608,9 +641,63 @@ private static Object safeMethod(Object target, String methodName, Object[] args
return null;
}
+ /**
+ * @param property
+ * @return
+ * @deprecated no need to convert, just get the Boolean object directly out of the store.
+ */
@Deprecated
public static boolean toBoolean(Object property) {
return "true".equals(StorageClientUtils.toString(property));
}
+ /**
+ * Delete an entire tree starting from the deepest part of the tree and
+ * working back up. Will stop the moment a permission denied is encountered
+ * either for read or for delete.
+ *
+ * @param contentManager
+ * @param path
+ * @throws AccessDeniedException
+ * @throws StorageClientException
+ */
+ public static void deleteTree(ContentManager contentManager, String path)
+ throws AccessDeniedException, StorageClientException {
+ Content content = contentManager.get(path);
+ if (content != null) {
+ for (String childPath : content.listChildPaths()) {
+ deleteTree(contentManager, childPath);
+ }
+ }
+ contentManager.delete(path);
+ }
+
+ public static String getInternalUuid() {
+ return getUuid()+"+"; // URL safe base 64 does not use + chars
+ }
+
+ public static void copyTree(ContentManager contentManager, String sourcePath, String destPath,
+ boolean withStreams) throws StorageClientException, AccessDeniedException, IOException {
+ contentManager.copy(sourcePath, destPath, withStreams);
+ LOGGER.info("Copied {} to {} ", sourcePath, destPath );
+ Content content = contentManager.get(sourcePath);
+ if (content != null) {
+ for (String childPath : content.listChildPaths()) {
+ String name = StorageClientUtils.getObjectName(childPath);
+ String childSourcePath = StorageClientUtils.newPath(sourcePath, name);
+ String childDestPath = StorageClientUtils.newPath(destPath, name);
+ copyTree(contentManager, childSourcePath, childDestPath, withStreams);
+ }
+ }
+ }
+
+ public static void dumpTree(Content content) {
+ if ( content != null ) {
+ LOGGER.info("Path {} ",content.getPath());
+ for ( Content child : content.listChildren() ) {
+ dumpTree(child);
+ }
+ }
+ }
+
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageConstants.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageConstants.java
new file mode 100644
index 00000000..542de652
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/StorageConstants.java
@@ -0,0 +1,59 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite;
+
+public class StorageConstants {
+
+ /**
+ * Property used to select a set of query statements in the finder. These must exist in
+ * the driver configuration and are intended to allow institutions to optimize certain
+ * queries. If not present, a the default set will be used.
+ */
+ public static final String CUSTOM_STATEMENT_SET = "_statementset";
+
+ /**
+ * Property used to set the maximum number of items a query should return per page.
+ * The starting row of the query is determined by the page number.
+ * Defaults to 25.
+ */
+ public static final String ITEMS = "_items";
+
+
+ /**
+ * Page number to start at, defaults to 0.
+ */
+ public static final String PAGE = "_page";
+
+ /**
+ * The column on which to perform a sort.
+ */
+ public static final String SORT = "_sort";
+
+ /**
+ * If present Raw Results will be returned as string values for each record.
+ */
+ public static final String RAWRESULTS = "_rawresults";
+
+ /**
+ * If true, then the query cache may be used. key-value pairs of the query must uniquely identify the query, and
+ * cache must be cleared by other means. This is a big ask and requires effort.
+ */
+ public static final String CACHEABLE = "_cacheable";
+
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/StoreListener.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/StoreListener.java
new file mode 100644
index 00000000..a9f2d3ed
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/StoreListener.java
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite;
+
+import java.util.Map;
+
+/**
+ * The StorageListener is notified when actions are performed objects in storage.
+ */
+public interface StoreListener {
+ public static final String TOPIC_BASE = "org/sakaiproject/nakamura/lite/";
+ public static final String DELETE_TOPIC = "DELETE";
+ public static final String ADDED_TOPIC = "ADDED";
+ public static final String UPDATED_TOPIC = "UPDATED";
+ public static final String DEFAULT_DELETE_TOPIC = TOPIC_BASE + DELETE_TOPIC;
+ public static final String DEFAULT_CREATE_TOPIC = TOPIC_BASE + ADDED_TOPIC;
+ public static final String DEFAULT_UPDATE_TOPIC = TOPIC_BASE + UPDATED_TOPIC;
+ public static final String[] DEFAULT_TOPICS = new String[] { DEFAULT_CREATE_TOPIC,
+ DEFAULT_UPDATE_TOPIC, DEFAULT_DELETE_TOPIC,
+ TOPIC_BASE + "authorizables/" + DELETE_TOPIC,
+ TOPIC_BASE + "groups/" + DELETE_TOPIC,
+ TOPIC_BASE + "users/" + DELETE_TOPIC,
+ TOPIC_BASE + "admin/" + DELETE_TOPIC,
+ TOPIC_BASE + "authorizables/" + DELETE_TOPIC,
+ TOPIC_BASE + "content/" + DELETE_TOPIC,
+ TOPIC_BASE + "authorizables/"+ADDED_TOPIC,
+ TOPIC_BASE + "groups/"+ADDED_TOPIC,
+ TOPIC_BASE + "users/"+ADDED_TOPIC,
+ TOPIC_BASE + "admin/"+ADDED_TOPIC,
+ TOPIC_BASE + "authorizables/"+ADDED_TOPIC,
+ TOPIC_BASE + "content/"+ADDED_TOPIC,
+ TOPIC_BASE + "authorizables/"+UPDATED_TOPIC,
+ TOPIC_BASE + "groups/"+UPDATED_TOPIC,
+ TOPIC_BASE + "users/"+UPDATED_TOPIC,
+ TOPIC_BASE + "admin/"+UPDATED_TOPIC,
+ TOPIC_BASE + "authorizables/"+UPDATED_TOPIC,
+ TOPIC_BASE + "content/"+UPDATED_TOPIC };
+ public static final String USERID_PROPERTY = "userid";
+ public static final String PATH_PROPERTY = "path";
+ public static final String RESOURCE_TYPE_PROPERTY = "resourceType";
+ public static final String BEFORE_EVENT_PROPERTY = "_beforeEvent";
+
+ /**
+ * onDelete is called after an object has been deleted.
+ * @param zone an identifier for the type of object being acted upon
+ * @param path the path to the object
+ * @param user the user logged in causing this action
+ * @param resourceType the resource type of the item, if known.
+ * @param beforeEvent the properties of the object before it was deleted
+ * @param attributes properties of the event itself
+ */
+ void onDelete(String zone, String path, String user, String resourceType, Map beforeEvent, String... attributes);
+
+ /**
+ * onUpdate is called after an object has been updated.
+ * @param zone an identifier for the type of object being acted upon
+ * @param path the path to the object
+ * @param user the user logged in causing this action
+ * @param resourceType the resource type of the item, if known.
+ * @param beforeEvent the properties of the object before it was updated
+ * @param attributes properties of the event itself
+ */
+ void onUpdate(String zone, String path, String user, String resourceType, boolean isNew, Map beforeEvent, String... attributes);
+
+ /**
+ * onLogin is called when a user logs in and creates a new {@link Session}
+ * @param userid
+ * @param sessionID
+ */
+ void onLogin(String userid, String sessionID);
+
+ /**
+ * onLogout is called when a user logs out of their {@link Session}
+ * @param userid
+ * @param sessionID
+ */
+ void onLogout(String userid, String sessionID);
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AccessControlManager.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AccessControlManager.java
similarity index 70%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AccessControlManager.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AccessControlManager.java
index 50c4de96..76c6c15b 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AccessControlManager.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AccessControlManager.java
@@ -19,6 +19,8 @@
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.authorizable.Authorizable;
+import org.sakaiproject.nakamura.api.lite.content.Content;
+import org.sakaiproject.nakamura.lite.accesscontrol.PropertyAcl;
import java.util.Map;
@@ -27,6 +29,20 @@
*/
public interface AccessControlManager {
+ /**
+ * Dynamic ACEs keys start with this value. Everything after the _tp_ is
+ * interpreted by the PrincipalTokenResolver to load a Content item
+ * containing the principal data. The content item must validate against the
+ * ACL item to be used.
+ */
+ public static final String DYNAMIC_PRINCIPAL_STEM = "_tp_";
+
+ /**
+ * Property ACEs start with this value. Property ACEs have the form
+ * _pp_@@
+ */
+ public static final String PROPERTY_PRINCIPAL_STEM = "_pp_";
+
/**
* Get an ACL at an object of a defined type. Do not look at parent objects
*
@@ -136,4 +152,47 @@ boolean can(Authorizable authorizable, String objectType, String objectPath,
String[] findPrincipals(String objectType, String objectPath, int permission, boolean granted) throws StorageClientException;
+ /**
+ * Bind a PrincipalTokenResolver to the Access Manager request.
+ * @param principalTokenResolver the principal resolver to use with this acl request.
+ */
+ void setRequestPrincipalResolver(PrincipalTokenResolver principalTokenResolver);
+
+
+ /**
+ * Unbind a PrincipalTokenResolver from the Access Manager.
+ */
+ void clearRequestPrincipalResolver();
+
+
+ /**
+ * This methods signs a token with the shared Key of the objectPath Content
+ * ACL and modifies the token properties with the signature. (using a HMAC
+ * based signature). It is the responsibility of the calling code to save
+ * the modified token.
+ *
+ * @param token
+ * the token to be signed
+ * @param objectType
+ * the type of the ACL path.
+ * @param objectPath
+ * the ACL path to use for signing
+ * @throws StorageClientException
+ * @throws AccessDeniedException
+ */
+ void signContentToken(Content token, String objectType, String objectPath) throws StorageClientException,
+ AccessDeniedException;
+
+
+ /**
+ * Get the property ACL applicable to the current user on the specified path.
+ * @param objectType the type of the object
+ * @param objectPath the path to the object
+ * @return
+ * @throws AccessDeniedException
+ * @throws StorageClientException
+ */
+ PropertyAcl getPropertyAcl(String objectType, String objectPath) throws AccessDeniedException, StorageClientException;
+
+
}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AccessDeniedException.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AccessDeniedException.java
similarity index 100%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AccessDeniedException.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AccessDeniedException.java
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AclModification.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AclModification.java
similarity index 80%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AclModification.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AclModification.java
index 4803c99b..1a69c8ae 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AclModification.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/AclModification.java
@@ -1,3 +1,20 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
package org.sakaiproject.nakamura.api.lite.accesscontrol;
import com.google.common.collect.Lists;
@@ -6,6 +23,7 @@
import java.util.Map;
import java.util.Map.Entry;
+
/**
* Specification of a modification to be applied to an ACL.
*/
@@ -186,7 +204,13 @@ public static String getPrincipal(String principalKey) {
if (principalKey.length() <= GRANTED_MARKER.length()) {
return null;
}
- return principalKey.substring(0, principalKey.length()-GRANTED_MARKER.length());
+ if ( principalKey.endsWith(GRANTED_MARKER) ) {
+ return principalKey.substring(0, principalKey.length()-GRANTED_MARKER.length());
+ } else if ( principalKey.endsWith(DENIED_MARKER) ) {
+ return principalKey.substring(0, principalKey.length()-DENIED_MARKER.length());
+ } else {
+ return null;
+ }
}
@@ -201,4 +225,8 @@ public static Permission[] listPermissions(int perms) {
return permissions.toArray(new Permission[permissions.size()]);
}
+ public static String getPropertyKey(String id, String propertyName) {
+ return AccessControlManager.PROPERTY_PRINCIPAL_STEM+id+"@"+propertyName;
+ }
+
}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Authenticator.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Authenticator.java
similarity index 82%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Authenticator.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Authenticator.java
index b1ff6fcf..fa8f412d 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Authenticator.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Authenticator.java
@@ -21,6 +21,7 @@
/**
* Authenticates a user
+ * @since 1.0
*/
public interface Authenticator {
@@ -33,14 +34,24 @@ public interface Authenticator {
* password for the user
* @return the user object for the user or null if the authentication
* attempt is not valid.
+ * @since 1.0
*/
User authenticate(String userid, String password);
/**
- * perform a system authentiation, trusting the userId.
+ * perform a system authentication, trusting the userId.
* @param userid
* @return the User object if the userID exists.
+ * @since 1.0
*/
User systemAuthenticate(String userid);
+ /**
+ * perform a system authentication bypassing enable login checks
+ * @param userid
+ * @return
+ * @since 1.4
+ */
+ User systemAuthenticateBypassEnable(String userid);
+
}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Permission.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Permission.java
similarity index 100%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Permission.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Permission.java
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Permissions.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Permissions.java
similarity index 91%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Permissions.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Permissions.java
index d14990b1..d86629b6 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Permissions.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Permissions.java
@@ -31,6 +31,9 @@ public class Permissions {
public static final Permission CAN_WRITE = new Permission(0x0002, "Write");
public static final Permission CAN_DELETE = new Permission(0x0004, "Delete");
public static final Permission CAN_ANYTHING = CAN_READ.combine(CAN_WRITE).combine(CAN_DELETE);
+ public static final Permission CAN_READ_PROPERTY = new Permission(0x0010, "Read Property");
+ public static final Permission CAN_WRITE_PROPERTY = new Permission(0x0020, "Write Property");
+ public static final Permission CAN_ANYTHING_PROPERTY = CAN_READ_PROPERTY.combine(CAN_WRITE_PROPERTY);
public static final Permission CAN_READ_ACL = new Permission(0x1000, "Read ACL");
public static final Permission CAN_WRITE_ACL = new Permission(0x2000, "Write ACL");
public static final Permission CAN_DELETE_ACL = new Permission(0x4000, "Delete ACL");
@@ -68,12 +71,14 @@ private static Map createConvertToSparsePermissions() {
b.put("write", Permissions.CAN_WRITE);
b.put("delete", Permissions.CAN_DELETE);
b.put("view", Permissions.CAN_READ);
- b.put("manage", Permissions.CAN_MANAGE);
- b.put("all", Permissions.ALL);
+ b.put("anything", Permissions.CAN_ANYTHING);
b.put("read-acl", Permissions.CAN_READ_ACL);
b.put("write-acl", Permissions.CAN_WRITE_ACL);
b.put("delete-acl", Permissions.CAN_DELETE_ACL);
b.put("manage-acl", Permissions.CAN_ANYTHING_ACL);
+ b.put("anything-acl", Permissions.CAN_ANYTHING_ACL);
+ b.put("manage", Permissions.CAN_MANAGE);
+ b.put("all", Permissions.ALL);
return b.build();
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/PrincipalTokenResolver.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/PrincipalTokenResolver.java
new file mode 100644
index 00000000..509db16f
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/PrincipalTokenResolver.java
@@ -0,0 +1,40 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite.accesscontrol;
+
+import org.sakaiproject.nakamura.api.lite.content.Content;
+
+import java.util.List;
+
+/**
+ * Resolves proxyPrincipals to tokens. An implementation of this will be
+ * provided by the caller if principal tokens are to be resolved. This
+ * implementation should bind to the user in question.
+ */
+public interface PrincipalTokenResolver {
+
+ /**
+ * Resolve the principal.
+ *
+ * @param principal
+ * @return the tokens associated with the proxyPrincipal, could be more than
+ * one.
+ */
+ List resolveTokens(String principal);
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/PrincipalValidatorPlugin.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/PrincipalValidatorPlugin.java
new file mode 100644
index 00000000..5e927bbb
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/PrincipalValidatorPlugin.java
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite.accesscontrol;
+
+import org.sakaiproject.nakamura.api.lite.content.Content;
+
+/**
+ * Validates a principal Token.
+ */
+public interface PrincipalValidatorPlugin {
+
+ /**
+ * Validate the token to see if its current. This should not need to consider
+ * the user since if the user is relevant they will have access to the token,
+ * if not, the token would not have been resolved for the user.
+ *
+ * @param proxyPrincipalToken
+ * @return true if the principal is valid, and the user who resolved it can
+ * have the principal.
+ */
+ boolean validate(Content proxyPrincipalToken);
+
+ /**
+ * @return a list of fields that must be protected, these are incorporated
+ * into the hmac to ensure no tampering.
+ */
+ String[] getProtectedFields();
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/PrincipalValidatorResolver.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/PrincipalValidatorResolver.java
new file mode 100644
index 00000000..a4f34df9
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/PrincipalValidatorResolver.java
@@ -0,0 +1,49 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite.accesscontrol;
+
+/**
+ * Resolves a Key to a PrincipalValidatorPlugin, and provides a location for
+ * Plugins to register. Plugins should depend on this service so they can
+ * register.
+ */
+public interface PrincipalValidatorResolver {
+
+ /**
+ * @param key
+ * the name of the plugin
+ * @return the plugin, or null if not found.
+ */
+ PrincipalValidatorPlugin getPluginByName(String key);
+
+ /**
+ * Register a plugin.
+ *
+ * @param key
+ * @param plugin
+ */
+ void registerPlugin(String key, PrincipalValidatorPlugin plugin);
+
+ /**
+ * De-register a plugin.
+ *
+ * @param key
+ */
+ void unregisterPlugin(String key);
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Security.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Security.java
similarity index 100%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Security.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/accesscontrol/Security.java
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/Authorizable.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/Authorizable.java
new file mode 100644
index 00000000..15ba217d
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/Authorizable.java
@@ -0,0 +1,490 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite.authorizable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang.StringUtils;
+import org.sakaiproject.nakamura.api.lite.RemoveProperty;
+import org.sakaiproject.nakamura.api.lite.Session;
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessControlManager;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.Security;
+import org.sakaiproject.nakamura.api.lite.util.Iterables;
+import org.sakaiproject.nakamura.api.lite.util.PreemptiveIterator;
+import org.sakaiproject.nakamura.lite.accesscontrol.AccessControlledMap;
+import org.sakaiproject.nakamura.lite.accesscontrol.PropertyAcl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+/**
+ * Base Authorizable object.
+ */
+public class Authorizable {
+
+ public static final String PASSWORD_FIELD = "pwd";
+
+ /**
+ * List of principals that this Authorizable has.
+ */
+ public static final String PRINCIPALS_FIELD = "principals";
+
+ /**
+ * List of members that are members of this authorizable.
+ */
+ public static final String MEMBERS_FIELD = "members";
+
+ /**
+ * The ID of the authorizable.
+ */
+ public static final String ID_FIELD = "id";
+
+ /**
+ * The name of the authorizable.
+ */
+ public static final String NAME_FIELD = "name";
+
+ /**
+ * The type of the authorizable, either g or u (Group or User)
+ */
+ public static final String AUTHORIZABLE_TYPE_FIELD = "type";
+
+ /**
+ * The type value indicating a group.
+ */
+ public static final String GROUP_VALUE = "g";
+ /**
+ * The type value indicating a user.
+ */
+ public static final String USER_VALUE = "u";
+
+ /**
+ * The name of the administrators group, members of which are granted access
+ * to certain functions.
+ */
+ public static final String ADMINISTRATORS_GROUP = "administrators";
+
+ /**
+ * The time (epoch long) the authroizable was modified.
+ */
+ public static final String LASTMODIFIED_FIELD = "lastModified";
+ /**
+ * The ID of the authorizable that last modified this authorizable.
+ */
+ public static final String LASTMODIFIED_BY_FIELD = "lastModifiedBy";
+ /**
+ * The time (epoch long) when the authorizable was created.
+ */
+ public static final String CREATED_FIELD = "created";
+ /**
+ * The ID of the authorizable that created this authorizable.
+ */
+ public static final String CREATED_BY_FIELD = "createdBy";
+
+ /**
+ * If the fields is set, then it defines the period during which the user
+ * may login. The fields upto 2 ISO8601 formatted dates, defining the start
+ * and end periods. If the value starts with a , eg ,2011-12-10 then the
+ * period is assumed to end on the date provided. If it ends with a , eg
+ * 2011-12-10, then the period starts on the date provided. If the date
+ * contains no time, the period is for the day in the timezone of the server
+ * time. If the period contains a time then the its precise.
+ */
+ public static final String LOGIN_ENABLED_PERIOD_FIELD = "loginEnabledPeriod";
+
+
+ /**
+ * A set of properties to filter out when sending out and setting.
+ */
+ private static final Set FILTER_PROPERTIES = ImmutableSet.of(PASSWORD_FIELD, ID_FIELD);
+
+ /**
+ * A set of properties that are not visiable.
+ */
+ private static final Set PRIVATE_PROPERTIES = ImmutableSet.of(PASSWORD_FIELD);
+
+ /**
+ * no password value.
+ */
+ public static final String NO_PASSWORD = "--none--";
+
+ protected static final Logger LOGGER = LoggerFactory.getLogger(Authorizable.class);
+
+ private static final Set IMMUTABLE_AUTH_IDS = ImmutableSet.of(Group.EVERYONE);
+
+ /**
+ * A read only copy of the map, protected by an Immutable Wrapper
+ */
+ protected Map authorizableMap;
+ /**
+ * A set of principals that this Authorizable has.
+ */
+ protected Set principals;
+
+ /**
+ * The ID of this authorizable.
+ */
+ protected String id;
+
+ /**
+ * Modifications to the map.
+ */
+ protected Map modifiedMap;
+ /**
+ * true if the principals have been modified.
+ */
+ protected boolean principalsModified;
+
+ /**
+ * true if the object is new.
+ */
+ private boolean isObjectNew = true;
+
+ /**
+ * true if the object is read only.
+ */
+ protected boolean readOnly;
+
+ private boolean immutable;
+
+ /**
+ * The Acl at load time for properties on this authorizable.
+ */
+ private PropertyAcl propertyAcl;
+
+ public Authorizable(Map autorizableMap) throws StorageClientException, AccessDeniedException {
+ this(autorizableMap, null);
+ }
+ public Authorizable(Map authorizableMap, Session session) throws StorageClientException, AccessDeniedException {
+ principalsModified = false;
+ this.id = (String) authorizableMap.get(ID_FIELD);
+ if (id == null || id.charAt(0) == '_') {
+ LOGGER.warn("Authorizables cant be null or start with _ this {} will cause problems ", id);
+ }
+ if ( session != null && !User.ADMIN_USER.equals(session.getUserId()) ) {
+ AccessControlManager accessControlManager = session.getAccessControlManager();
+ propertyAcl = accessControlManager.getPropertyAcl(Security.ZONE_AUTHORIZABLES, id );
+ } else {
+ propertyAcl = new PropertyAcl();
+ }
+ modifiedMap = new AccessControlledMap(propertyAcl);
+ init(authorizableMap, propertyAcl);
+ }
+
+ private void init(Map newMap, PropertyAcl propertyAcl) {
+ this.authorizableMap = StorageClientUtils.getFilterMap(newMap, null, null, propertyAcl.readDeniedSet(), false);
+ Object principalsB = authorizableMap.get(PRINCIPALS_FIELD);
+ if (principalsB == null) {
+ this.principals = Sets.newLinkedHashSet();
+ } else {
+ this.principals = Sets.newLinkedHashSet(Iterables.of(StringUtils.split(
+ (String) principalsB, ';')));
+ }
+ if (!User.ANON_USER.equals(this.id)) {
+ this.principals.add(Group.EVERYONE);
+ }
+ }
+
+ /**
+ * @param newMap
+ * the new map to reset the authorizable to.
+ */
+ public void reset(Map newMap) {
+ if (!readOnly) {
+ principalsModified = false;
+ modifiedMap.clear();
+ init(newMap, propertyAcl);
+
+ LOGGER.debug("After Update to Authorizable {} ", authorizableMap);
+ }
+ }
+
+ /**
+ * @return an array of principals that the authorizable has, indicating the
+ * groups that the authorizable is a member of and any other
+ * principals that have been granted to this authorizable.
+ * Principals are generally use in access control list and are not
+ * limited to group ids.
+ */
+ public String[] getPrincipals() {
+ return principals.toArray(new String[principals.size()]);
+ }
+
+ /**
+ * @return the ID of this authorizable (immutable)
+ */
+ public String getId() {
+ return id;
+ }
+
+ // TODO: Unit test
+ /**
+ * @return get the current set of safe properties that can be updated, laking into account any modifications.
+ */
+ public Map getSafeProperties() {
+ if (!readOnly && principalsModified) {
+ modifiedMap.put(PRINCIPALS_FIELD, StringUtils.join(principals, ';'));
+ }
+ return StorageClientUtils.getFilterMap(authorizableMap, modifiedMap, null,
+ FILTER_PROPERTIES, false);
+ }
+
+ /**
+ * Returns the properties of the authorizable taking into account any modifications. This includes fields that could be modified.
+ * @return
+ */
+ public Map getProperties() {
+ if (!readOnly && principalsModified) {
+ modifiedMap.put(PRINCIPALS_FIELD, StringUtils.join(principals, ';'));
+ }
+ return StorageClientUtils.getFilterMap(authorizableMap, modifiedMap, null,
+ PRIVATE_PROPERTIES, false);
+ }
+
+ /**
+ * @return true if this authorizable is a group.
+ */
+ public boolean isGroup() {
+ return false;
+ }
+
+ /**
+ * @return get the orriginal properties of this authorizable ignoring any unsaved properties.
+ */
+ public Map getOriginalProperties() {
+ return StorageClientUtils.getFilterMap(authorizableMap, null, null, FILTER_PROPERTIES, false);
+ }
+
+ /**
+ * Set a property. The property will only be set if writable. If the property or this athorizable is read only, nothing will happen.
+ * @param name the name of the property
+ * @param value the value of the property.
+ */
+ public void setProperty(String name, Object value) {
+ if (!readOnly && !FILTER_PROPERTIES.contains(name)) {
+ Object cv = authorizableMap.get(name);
+ if ( value == null ) {
+ if ( cv != null && !(cv instanceof RemoveProperty)) {
+ modifiedMap.put(name, new RemoveProperty());
+ }
+ } else if (!value.equals(cv)) {
+ modifiedMap.put(name, value);
+ } else if (modifiedMap.containsKey(name) && !value.equals(modifiedMap.get(name))) {
+ modifiedMap.put(name, value);
+ }
+
+ }
+ }
+
+ /**
+ * @param name
+ * @return the instance of the property. Note that if the property is an array or object it will be mutable.
+ */
+ public Object getProperty(String name) {
+ if (!PRIVATE_PROPERTIES.contains(name)) {
+ if (modifiedMap.containsKey(name)) {
+ Object o = modifiedMap.get(name);
+ if (o instanceof RemoveProperty) {
+ return null;
+ } else {
+ return o;
+ }
+ }
+ return authorizableMap.get(name);
+ }
+ return null;
+ }
+
+ /**
+ * remove the property.
+ * @param name
+ */
+ public void removeProperty(String key) {
+ if (!readOnly && (authorizableMap.containsKey(key) || modifiedMap.containsKey(key))) {
+ modifiedMap.put(key, new RemoveProperty());
+ }
+ }
+
+ /**
+ * add a principal to this authorizable.
+ * @param principal
+ */
+ public void addPrincipal(String principal) {
+ if (!readOnly && !principals.contains(principal)) {
+ principals.add(principal);
+ principalsModified = true;
+ }
+ }
+
+ /**
+ * remove a principal from this authorizable.
+ * @param principal
+ */
+ public void removePrincipal(String principal) {
+ if (!readOnly && principals.contains(principal)) {
+ principals.remove(principal);
+ principalsModified = true;
+ }
+ }
+
+ /**
+ * @return a Map or properties that should be saved to storage. This merges the original properties and unsaved changed.
+ */
+ public Map getPropertiesForUpdate() {
+ if (!readOnly && principalsModified) {
+ principals.remove(Group.EVERYONE);
+ modifiedMap.put(PRINCIPALS_FIELD, StringUtils.join(principals, ';'));
+ principals.add(Group.EVERYONE);
+ }
+ return StorageClientUtils.getFilterMap(authorizableMap, modifiedMap, null,
+ FILTER_PROPERTIES, true);
+ }
+
+ /**
+ * @return true if the authorizable is modified.
+ */
+ public boolean isModified() {
+ return !readOnly && (principalsModified || (modifiedMap.size() > 0));
+ }
+
+ /**
+ * @param name
+ * @return true if the property is set in the unsaved version of the authorizable.
+ */
+ public boolean hasProperty(String name) {
+ Object modifiedValue = modifiedMap.get(name);
+ if (modifiedValue instanceof RemoveProperty) {
+ return false;
+ }
+ if (modifiedValue != null) {
+ return true;
+ }
+ return authorizableMap.containsKey(name);
+ }
+
+ /**
+ * @param authorizableManager
+ * @return an Iterator containing Groups this authorizable is a direct or
+ * indirect member of.
+ */
+ public Iterator memberOf(final AuthorizableManager authorizableManager) {
+ final List memberIds = new ArrayList();
+ Collections.addAll(memberIds, getPrincipals());
+ return new PreemptiveIterator() {
+
+ private int p;
+ private Group group;
+
+ @Override
+ protected boolean internalHasNext() {
+ while (p < memberIds.size()) {
+ String id = memberIds.get(p);
+ p++;
+ try {
+ Authorizable a = authorizableManager.findAuthorizable(id);
+ if (a instanceof Group) {
+ group = (Group) a;
+ for (String pid : a.getPrincipals()) {
+ if (!memberIds.contains(pid)) {
+ memberIds.add(pid);
+ }
+ }
+ return true;
+ }
+ } catch (AccessDeniedException e) {
+ LOGGER.debug(e.getMessage(), e);
+ } catch (StorageClientException e) {
+ LOGGER.debug(e.getMessage(), e);
+ }
+ }
+ close();
+ return false;
+ }
+
+ @Override
+ protected Group internalNext() {
+ return group;
+ }
+
+ };
+ }
+
+ /**
+ * @param isObjectNew mark the object as new.
+ */
+ protected void setObjectNew(boolean isObjectNew) {
+ this.isObjectNew = isObjectNew;
+ }
+
+ /**
+ * @return true if the object is new.
+ */
+ public boolean isNew() {
+ return isObjectNew;
+ }
+
+ /**
+ * @param readOnly mark the object read only.
+ */
+ protected void setReadOnly(boolean readOnly) {
+ if (!this.readOnly) {
+ this.readOnly = readOnly;
+ }
+ }
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Authorizable) {
+ Authorizable a = (Authorizable) obj;
+ return id.equals(a.getId());
+ }
+ return super.equals(obj);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return id;
+ }
+
+ public boolean isImmutable() {
+ return immutable || IMMUTABLE_AUTH_IDS.contains(id);
+ }
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/AuthorizableManager.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/AuthorizableManager.java
similarity index 74%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/AuthorizableManager.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/AuthorizableManager.java
index 92ff79d1..38b12df4 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/AuthorizableManager.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/AuthorizableManager.java
@@ -49,6 +49,16 @@ Authorizable findAuthorizable(String authorizableId) throws AccessDeniedExceptio
void updateAuthorizable(Authorizable authorizable) throws AccessDeniedException,
StorageClientException;
+ /**
+ * Update an authorizable with the option to not touch the user last modified information.
+ * @param authorizable the authorizable.
+ * @param withTouch if false the last modified information will not be changed, but only admin users can perform this.
+ * @throws AccessDeniedException
+ * @throws StorageClientException
+ */
+ void updateAuthorizable(Authorizable authorizable, boolean withTouch)
+ throws AccessDeniedException, StorageClientException;
+
/**
* Create a group
* @param authorizableId the group ID
@@ -94,6 +104,16 @@ boolean createUser(String userId, String userName, String password,
void changePassword(Authorizable authorizable, String password, String oldPassword)
throws StorageClientException, AccessDeniedException;
+
+ /**
+ * Administratively disable a password for the supplied user. Only admin can do this.
+ * @param authorizable
+ * @throws StorageClientException
+ * @throws AccessDeniedException
+ */
+ void disablePassword(Authorizable authorizable)
+ throws StorageClientException, AccessDeniedException;
+
/**
* Find authorizables by exact property matches
* @param propertyName the name of the property
@@ -105,4 +125,25 @@ void changePassword(Authorizable authorizable, String password, String oldPasswo
Iterator findAuthorizable(String propertyName, String value,
Class extends Authorizable> authorizableType) throws StorageClientException;
+ /**
+ * @return the user bound to this authorizable manager.
+ */
+ User getUser();
+
+
+ /**
+ * @param path cause an event to be emitted for the path that will cause a refresh.
+ * @throws AccessDeniedException
+ * @throws StorageClientException
+ */
+ void triggerRefresh(String path) throws StorageClientException, AccessDeniedException;
+
+
+ /**
+ * Cause an event to be emitted for all items.
+ * @throws StorageClientException
+ */
+ void triggerRefreshAll() throws StorageClientException;
+
+
}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/Group.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/Group.java
similarity index 65%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/Group.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/Group.java
index 73fd3512..3d87ec6d 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/Group.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/Group.java
@@ -21,6 +21,9 @@
import com.google.common.collect.Sets;
import org.apache.commons.lang.StringUtils;
+import org.sakaiproject.nakamura.api.lite.Session;
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.util.Iterables;
import org.sakaiproject.nakamura.lite.authorizable.GroupInternal;
@@ -28,7 +31,7 @@
import java.util.Set;
/**
- * A group has a list of members that is maintaiend in the group. This is
+ * A group has a list of members that is maintained in the group. This is
* reflected as principals in each member, managed by the AuthorizableManager,
* only updated on save.
*
@@ -41,46 +44,71 @@ public class Group extends Authorizable {
* The ID of the everyone group. Includes all users except anon.
*/
public static final String EVERYONE = "everyone";
- public static final Group EVERYONE_GROUP = new GroupInternal(ImmutableMap.of("id",(Object)EVERYONE), false, true);
+ public static final Group EVERYONE_GROUP = getEveryone();
private Set members;
private Set membersAdded;
private Set membersRemoved;
private boolean membersModified;
- public Group(Map groupMap) {
- super(groupMap);
+ public Group(Map groupMap) throws StorageClientException, AccessDeniedException {
+ this(groupMap, null);
+ }
+
+ public Group(Map groupMap, Session session) throws StorageClientException, AccessDeniedException {
+ super(groupMap, session);
this.members = Sets.newLinkedHashSet(Iterables.of(StringUtils.split(
(String) authorizableMap.get(MEMBERS_FIELD), ';')));
this.membersAdded = Sets.newHashSet();
this.membersRemoved = Sets.newHashSet();
membersModified = true;
}
-
+ private static Group getEveryone() {
+ try {
+ return new GroupInternal(ImmutableMap.of("id", (Object) EVERYONE), null, false, true);
+ } catch (StorageClientException e) {
+ // it cant throw this since the session is null
+ } catch (AccessDeniedException e) {
+ // it cant throw this since the session is null
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
@Override
public boolean isGroup() {
return true;
}
-
+ /**
+ * {@inheritDoc}
+ */
@Override
public Map getPropertiesForUpdate() {
- if ( !readOnly && membersModified ) {
+ if (!readOnly && membersModified) {
modifiedMap.put(MEMBERS_FIELD, StringUtils.join(members, ';'));
}
- Map propertiesForUpdate = super.getPropertiesForUpdate();
+ Map propertiesForUpdate = super.getPropertiesForUpdate();
return propertiesForUpdate;
}
-
+
+ /**
+ * {@inheritDoc}
+ */
@Override
// TODO: Unit test
public Map getSafeProperties() {
- if ( !readOnly && membersModified ) {
+ if (!readOnly && membersModified) {
modifiedMap.put(MEMBERS_FIELD, StringUtils.join(members, ';'));
}
return super.getSafeProperties();
}
-
+
+ /**
+ * {@inheritDoc}
+ */
@Override
// TODO: Unit test
public boolean isModified() {
@@ -93,25 +121,27 @@ public String[] getMembers() {
public void addMember(String member) {
if (!readOnly && !members.contains(member)) {
- LOGGER.debug(" {} adding Member {} to {} ",new Object[]{this,member, members});
+ LOGGER.debug(" {} adding Member {} to {} ", new Object[] { this, member, members });
members.add(member);
membersAdded.add(member);
membersRemoved.remove(member);
membersModified = true;
} else {
- LOGGER.debug("{} Member {} already present in {} ",new Object[]{this,member,members});
+ LOGGER.debug("{} Member {} already present in {} ", new Object[] { this, member,
+ members });
}
}
public void removeMember(String member) {
if (!readOnly && members.contains(member)) {
- LOGGER.debug(" {} removing Member {} to {} ",new Object[]{this,member, members});
+ LOGGER.debug(" {} removing Member {} to {} ", new Object[] { this, member, members });
members.remove(member);
membersAdded.remove(member);
membersRemoved.add(member);
membersModified = true;
} else {
- LOGGER.debug("{} Member {} already not present in {} ",new Object[]{this,member,members});
+ LOGGER.debug("{} Member {} already not present in {} ", new Object[] { this, member,
+ members });
}
}
@@ -124,9 +154,9 @@ public String[] getMembersRemoved() {
}
public void reset(Map newMap) {
- if (!readOnly ) {
+ if (!readOnly) {
super.reset(newMap);
- LOGGER.debug("{} reset ",new Object[]{this});
+ LOGGER.debug("{} reset ", new Object[] { this });
this.members = Sets.newLinkedHashSet(Iterables.of(StringUtils.split(
(String) authorizableMap.get(MEMBERS_FIELD), ';')));
membersAdded.clear();
@@ -134,5 +164,15 @@ public void reset(Map newMap) {
membersModified = false;
}
}
+
+ @Override
+ public boolean equals(Object obj) {
+ return super.equals(obj); // Group and User shared the same key space.
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/User.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/User.java
similarity index 51%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/User.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/User.java
index 5ea86995..fbe453e4 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/User.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/authorizable/User.java
@@ -20,15 +20,21 @@
import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang.StringUtils;
+import org.sakaiproject.nakamura.api.lite.Session;
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
+import org.sakaiproject.nakamura.api.lite.util.EnabledPeriod;
import java.security.Principal;
+import java.util.Calendar;
import java.util.Map;
import java.util.Set;
+import java.util.TimeZone;
import javax.security.auth.Subject;
/**
- * Represetnation of the User.
+ * Representation of the User.
*/
public class User extends Authorizable {
@@ -41,13 +47,18 @@ public class User extends Authorizable {
*/
public static final String ANON_USER = "anonymous";
/**
- * The ID of teh system user.
+ * The ID of the system user.
*/
public static final String SYSTEM_USER = "system";
public static final String IMPERSONATORS_FIELD = "impersonators";
- public User(Map userMap) {
- super(userMap);
+ public User(Map userMap) throws StorageClientException, AccessDeniedException {
+ this(userMap, null);
+ }
+
+ public User(Map userMap, Session session) throws StorageClientException,
+ AccessDeniedException {
+ super(userMap, session);
}
/**
@@ -74,7 +85,7 @@ public boolean allowImpersonate(Subject impersSubject) {
if (impersonators == null) {
return false;
}
- Set impersonatorSet = ImmutableSet.of(StringUtils.split(impersonators, ';'));
+ Set impersonatorSet = ImmutableSet.copyOf(StringUtils.split(impersonators, ';'));
for (Principal p : impersSubject.getPrincipals()) {
if (ADMIN_USER.equals(p.getName()) || SYSTEM_USER.equals(p.getName())
@@ -85,4 +96,47 @@ public boolean allowImpersonate(Subject impersSubject) {
return false;
}
+ /**
+ * @return returns true if login is enabled for this user.
+ * @since 1.4
+ */
+ public boolean isLoginEnabled() {
+ return EnabledPeriod.isInEnabledPeriod((String) getProperty(LOGIN_ENABLED_PERIOD_FIELD));
+ }
+
+ /**
+ * Sets the login enabled time
+ *
+ * @param from
+ * UTC ms time after which user login is enabled. < 0 means no
+ * start time.
+ * @param to
+ * UTC ms time before which the user login is enabled, < 0 means
+ * no end time.
+ * @param day
+ * true if the time represents a day rather than a time
+ * @param timeZone
+ * the timezone which both these times should be interpreted in
+ * (relevant for a day setting).
+ * @since 1.4
+ */
+ public void setLoginEnabled(long from, long to, boolean day, TimeZone timeZone) {
+ String enabledSetting = EnabledPeriod.getEnableValue(from, to, day, timeZone);
+ if (enabledSetting == null) {
+ removeProperty(LOGIN_ENABLED_PERIOD_FIELD);
+ } else {
+ setProperty(LOGIN_ENABLED_PERIOD_FIELD, enabledSetting);
+ }
+ }
+
+ /**
+ * @return an array length 2 of the times when the user is enabled in order
+ * from to. null indicates no time specified for either from or to
+ * times. This user will be allowed to login between those times.
+ * @since 1.4
+ */
+ public Calendar[] getLoginEnabledPeriod() {
+ return EnabledPeriod.getEnabledPeriod((String) getProperty(LOGIN_ENABLED_PERIOD_FIELD));
+ }
+
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/content/ActionRecord.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/content/ActionRecord.java
new file mode 100644
index 00000000..f0d992c6
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/content/ActionRecord.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite.content;
+
+/**
+ * Supplemental object for tracking transactions where sub-nodes are included
+ *
+ * @param from
+ * - Original path of object
+ * @param to
+ * - Final path of object (can be null if transaction is delete)
+ */
+public class ActionRecord {
+ private String from;
+ private String to;
+
+ public ActionRecord(String newFrom, String newTo) {
+ from = newFrom;
+ to = newTo;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ public String getTo() {
+ return to;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/content/Content.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/content/Content.java
similarity index 71%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/content/Content.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/content/Content.java
index e5f9b095..8081d784 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/content/Content.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/content/Content.java
@@ -32,35 +32,46 @@
* already exist with a cotentManager.get(path), and if that responds with a
* null object, then create a new Content object with new Content(path, map);
* where path is the path of the content object and map is null or the initial
- * properties of the content object. At that poin the conten object is created
+ * properties of the content object. At that point the content object is created
* but not saved. To save perform contentManager.udpate(contentObject) which
- * will create any itermediate path and save the content object. At that point
+ * will create any intermediate path and save the content object. At that point
* it will be persisted in the content store and have structure objects.
*
*
+ * Please note, if you create a Content object using the public constructor,
+ * that object will have no children until it is saved and re-loaded by the
+ * ContentManager. Any attempt to list children of the newly created Content
+ * instance will result in an empty iterator.
+ *
+ *
* If you need to make changes to a Content object, get it out of the store,
* with contentManager.get(path); then change some properties before performing
* a contentManager.update(contentObject); Transactions are managed by the
* underlying store implementation and are not actively managed in the
- * cotnentManager. If your underlying store is not transactional, the update
+ * contentManager. If your underlying store is not transactional, the update
* operation will persist directly to the underlying store. Concurrent threads
* in the same JVM may retrieve the same underlying data from the content store
- * but each cotnentManager will operate on its own set of contentObjects
- * isolated from other cotnentManagers until the update operation is completed.
+ * but each contentManager will operate on its own set of contentObjects
+ * isolated from other contentManagers until the update operation is completed.
*
*/
public class Content extends InternalContent {
/**
* Create a brand new content object not connected to the underlying store.
- * To save use contentManager.update(contentObject);
+ * To save use contentManager.update(contentObject); Since the object is not
+ * connected to the underlying store, it not have any children. Only Content
+ * objects loaded from the underlying store with ContentManager.get(path)
+ * are connected to the underlying store and have children. This is the case
+ * even if the path of the Content instance created via the public
+ * constructor exists within the underlying content store.
*
* @param path
* the path in the store that should not already exist. If it
* does exist, this new object will overwrite.
* @param content
* a map of initial content metadata.
- * @param
+ * @param
*/
public Content(String path, java.util.Map content) {
super(path, content);
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/content/ContentManager.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/content/ContentManager.java
similarity index 64%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/content/ContentManager.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/content/ContentManager.java
index 92203a58..ebb146a0 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/content/ContentManager.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/content/ContentManager.java
@@ -19,10 +19,13 @@
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalTokenResolver;
import java.io.IOException;
import java.io.InputStream;
+import java.util.Iterator;
import java.util.List;
+import java.util.Map;
/**
* Defines a ContentManager service for operating on content.
@@ -45,6 +48,25 @@ public interface ContentManager {
*/
Content get(String path) throws StorageClientException, AccessDeniedException;
+ /**
+ * Perform a search for content matching the given properties
+ *
+ * @param searchProperties a Map of property names and values. All the properties must match to give a result
+ * @return an Iterable of Content items in no guaranteed order
+ * @throws StorageClientException
+ * @throws AccessDeniedException
+ */
+ // TODO needs better documentation - not clear how to OR or AND
+ Iterable find(Map searchProperties) throws StorageClientException, AccessDeniedException;
+
+ /**
+ * Counts the maximum number of results a find operation could return, ignoring access control. This method may cause problems
+ * if used inappropriately on sets of results that are mostly not readable by the current user (eg how many documents are there with "ieb" and "your fired" in ?)
+ * @param searchProperties Map the same as the finder
+ * @return maximum number of results a find could return.
+ */
+ int count(Map countSearch) throws StorageClientException;
+
/**
* Save the current version of the content object including metadata and
* file bodies as a read only snapshot
@@ -61,23 +83,44 @@ public interface ContentManager {
* at that location and possibly at parent locations.
*/
String saveVersion(String path) throws StorageClientException, AccessDeniedException;
+
+ String saveVersion(String path, Map versionMetadata) throws StorageClientException, AccessDeniedException;
/**
- * Update or create the content object, and intermediate path if necessary,
- * stored at the location indicated by the path of the content object
- * supplied.
- *
- * @param content
- * the content object to update.
- * @throws StorageClientException
- * if there was a problem with the operation.
- * @throws AccessDeniedException
- * if the user is unable to write the object at the path. This
- * is not an indication that the objected at the path exists,
- * just that the user can't write anything at that location and
- * possibly at parent locations.
- */
- void update(Content content) throws AccessDeniedException, StorageClientException;
+ * Update or create the content object, and intermediate path if necessary,
+ * stored at the location indicated by the path of the content object
+ * supplied.
+ *
+ * @param content
+ * the content object to update.
+ * @throws StorageClientException
+ * if there was a problem with the operation.
+ * @throws AccessDeniedException
+ * if the user is unable to write the object at the path. This
+ * is not an indication that the objected at the path exists,
+ * just that the user can't write anything at that location and
+ * possibly at parent locations.
+ */
+ void update(Content content) throws AccessDeniedException, StorageClientException;
+
+ /**
+ * Update or create the content object, and intermediate path if necessary,
+ * stored at the location indicated by the path of the content object
+ * supplied.
+ *
+ * @param content
+ * the content object to update.
+ * @param withTouch
+ * if false, the modification timestamp will not be updated. Only admin can use this option.
+ * @throws StorageClientException
+ * if there was a problem with the operation.
+ * @throws AccessDeniedException
+ * if the user is unable to write the object at the path. This
+ * is not an indication that the objected at the path exists,
+ * just that the user can't write anything at that location and
+ * possibly at parent locations.
+ */
+ void update(Content content, boolean withTouch) throws AccessDeniedException, StorageClientException;
/**
* Delete the content object at the path indicated.
@@ -197,7 +240,7 @@ InputStream getInputStream(String path, String streamId) throws StorageClientExc
* the path to copy from, must exist
* @param to
* the path to copy to, must not exist
- * @param deep
+ * @param withStreams
* if true, a copy is made of all the streams, if false the
* streams are shared but copies are made of the properties.
* @throws IOException
@@ -205,7 +248,7 @@ InputStream getInputStream(String path, String streamId) throws StorageClientExc
* if the user cant read the source or write the desination.
* @throws IOException
*/
- void copy(String from, String to, boolean deep) throws StorageClientException,
+ void copy(String from, String to, boolean withStreams) throws StorageClientException,
AccessDeniedException, IOException;
/**
@@ -218,22 +261,35 @@ void copy(String from, String to, boolean deep) throws StorageClientException,
* @throws StorageClientException
* @throws AccessDeniedException
*/
- void move(String from, String to) throws AccessDeniedException, StorageClientException;
+ List move(String from, String to) throws AccessDeniedException, StorageClientException;
- /**
- * Create a Link. Links place a pointer to real content located at the to
- * path, in the from path. Modifications to the underlying content are
- * reflected in both locations. Permissions are controlled by the location
- * and not the underlying content.
- *
- * @param from
- * the source of the link (the soft part), must not exist.
- * @param to
- * the destination, must exist
- * @throws AccessDeniedException
- * if the user cant read the to and write the from
- * @throws StorageClientException
- */
+ /**
+ * Move a content item from to.
+ *
+ * @param from
+ * the source, must exist
+ * @param to
+ * the destination must not exist.
+ * @param force
+ * Whether to forcefully move to the destination (i.e. overwrite)
+ * @throws StorageClientException
+ * @throws AccessDeniedException
+ */
+ List move(String from, String to, boolean force) throws AccessDeniedException, StorageClientException;
+
+ /**
+ * Create a Link. Links place a pointer to real content located at the to path, in the
+ * from path. Modifications to the underlying content are reflected in both locations.
+ * Permissions are controlled by the location and not the underlying content.
+ *
+ * @param from
+ * the source of the link (the soft part), must not exist.
+ * @param to
+ * the destination, must exist
+ * @throws AccessDeniedException
+ * if the user cant read the to and write the from
+ * @throws StorageClientException
+ */
void link(String from, String to) throws AccessDeniedException, StorageClientException;
/**
@@ -290,4 +346,61 @@ InputStream getVersionInputStream(String path, String versionId) throws AccessDe
List getVersionHistory(String path) throws AccessDeniedException,
StorageClientException;
+ /**
+ * Gets a lazy iterator of child paths.
+ * @param path the parent path.
+ * @return
+ * @throws StorageClientException
+ */
+ Iterator listChildPaths(String path) throws StorageClientException;
+
+ /**
+ * Get a lazy iterator of child content objects.
+ * @param path
+ * @return
+ * @throws StorageClientException
+ */
+ Iterator listChildren(String path) throws StorageClientException;
+
+ /**
+ * @param path the path of the content node
+ * @param streamId the stream id, null for the default stream
+ * @return true if the stream id is present.
+ * @throws AccessDeniedException
+ * @throws StorageClientException
+ */
+ boolean hasBody(String path, String streamId) throws StorageClientException, AccessDeniedException;
+
+ /**
+ * Sets the principal Token Resolver for all subsequent requests using this
+ * session. When the ContentManager is invoked it will consult the supplied
+ * principal Token Resolver to locate any extra tokens that have been
+ * granted.
+ *
+ * @param principalTokenResolver
+ */
+ void setPrincipalTokenResolver(PrincipalTokenResolver principalTokenResolver);
+
+ /**
+ * Clear the principal Token Resolver
+ */
+ void cleanPrincipalTokenResolver();
+
+
+ /**
+ * @param path cause an event to be emitted for the path that will cause a refresh.
+ * @throws AccessDeniedException
+ * @throws StorageClientException
+ */
+ void triggerRefresh(String path) throws StorageClientException, AccessDeniedException;
+
+
+ /**
+ * Cause an event to be emitted for all items.
+ * @throws StorageClientException
+ */
+ void triggerRefreshAll() throws StorageClientException;
+
+
+
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/AlreadyLockedException.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/AlreadyLockedException.java
new file mode 100644
index 00000000..a0bba217
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/AlreadyLockedException.java
@@ -0,0 +1,14 @@
+package org.sakaiproject.nakamura.api.lite.lock;
+
+public class AlreadyLockedException extends Exception {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -6198174336492911030L;
+
+ public AlreadyLockedException(String path) {
+ super("Lock path: "+path);
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockManager.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockManager.java
new file mode 100644
index 00000000..132d7e29
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockManager.java
@@ -0,0 +1,78 @@
+package org.sakaiproject.nakamura.api.lite.lock;
+
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+
+/**
+ * A simple hierarchical lock manager with tokens to identify locks.
+ * Implementations of the interface should not be bound to the content system.
+ * Locks live in their own hierarchy, and may exist even if there is not object
+ * present at that location in any other hierarchy.
+ *
+ * @author ieb
+ *
+ */
+public interface LockManager {
+
+ /**
+ * Locks a path returning a token for the lock if successful, null if not
+ *
+ * @param path
+ * the path to lock
+ * @param timeoutInSeconds
+ * ttl for the lock in s from the time it was created.
+ * @param extra
+ * any extra information to be stored with the lock.
+ * @return the lock token.
+ * @throws StorageClientException
+ * @throws AlreadyLockedException
+ */
+ String lock(String path, long timeoutInSeconds, String extra) throws StorageClientException,
+ AlreadyLockedException;
+
+ /**
+ * Unlock a path for a given token, if the token and current user match.
+ *
+ * @param path
+ * the path
+ * @param token
+ * the token.
+ * @throws StorageClientException
+ */
+ void unlock(String path, String token) throws StorageClientException;
+
+ /**
+ * Get the lock state for a path given a token
+ *
+ * @param path
+ * the path
+ * @param token
+ * the token
+ * @return a lock state object which indicates if the token is current and
+ * bound to the current user. Lock state also indicates the location
+ * of the current lock.
+ * @throws StorageClientException
+ */
+ LockState getLockState(String path, String token) throws StorageClientException;
+
+ /**
+ * Check the it path is locked.
+ *
+ * @param path
+ * the path.
+ * @return true if the path is locked.
+ * @throws StorageClientException
+ */
+ boolean isLocked(String path) throws StorageClientException;
+
+ /**
+ * Refresh the lock keeping the same token.
+ * @param path
+ * @param timeoutInSeconds
+ * @param string
+ * @param token
+ * @return the token, which should be the same.
+ * @throws StorageClientException
+ */
+ String refreshLock(String path, long timeoutInSeconds, String extra, String token) throws StorageClientException;
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockState.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockState.java
new file mode 100644
index 00000000..0874c9f2
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/lock/LockState.java
@@ -0,0 +1,79 @@
+package org.sakaiproject.nakamura.api.lite.lock;
+
+
+public class LockState {
+
+ private static final LockState NOT_LOCKED = new LockState(null, false, null, false, false,
+ null, null);
+ private final boolean isOwner;
+ private final String owner;
+ private final String path;
+ private final boolean locked;
+ private String token;
+ private String extra;
+ private boolean matchedToken;
+
+ public LockState(String path, boolean isOwner, String owner, boolean locked,
+ boolean matchedToken, String token, String extra) {
+ this.path = path;
+ this.isOwner = isOwner;
+ this.owner = owner;
+ this.locked = locked;
+ this.matchedToken = matchedToken;
+ this.token = token;
+ this.extra = extra;
+ }
+
+ public static LockState getOwnerLockedToken(String path, String owner, String token,
+ String extra) {
+ return new LockState(path, true, owner, true, true, token, extra);
+ }
+
+ public static LockState getOwnerLockedNoToken(String path, String owner, String token,
+ String extra) {
+ return new LockState(path, true, owner, true, false, token, extra);
+ }
+
+ public static LockState getUserLocked(String path, String owner, String token, String extra) {
+ return new LockState(path, false, owner, true, false, token, extra);
+ }
+
+ public static LockState getNotLocked() {
+ return NOT_LOCKED;
+ }
+
+ public boolean isOwner() {
+ return isOwner;
+ }
+
+ public String getLockPath() {
+ return path;
+ }
+
+ public boolean isLocked() {
+ return locked;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public boolean hasMatchedToken() {
+ return matchedToken;
+ }
+
+ public String getExtra() {
+ return extra;
+ }
+
+ public String getOwner() {
+ return owner;
+ }
+
+ @Override
+ public String toString() {
+ return " isOwner:" + isOwner + " owner:" + owner + " locked:" + locked + " matchedToken:"
+ + matchedToken + " token:" + token + " extra:[" + extra + "]";
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/EnabledPeriod.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/EnabledPeriod.java
new file mode 100644
index 00000000..90915c5f
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/EnabledPeriod.java
@@ -0,0 +1,71 @@
+package org.sakaiproject.nakamura.api.lite.util;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class EnabledPeriod {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(EnabledPeriod.class);
+
+ public static boolean isInEnabledPeriod(String enabledPeriod) {
+ Calendar[] period = getEnabledPeriod(enabledPeriod);
+ Calendar now = new ISO8601Date();
+ now.setTimeInMillis(System.currentTimeMillis());
+ if (period[0] != null && period[0].compareTo(now) > 0) {
+ return false;
+ }
+ if (period[1] != null && period[1].compareTo(now) <= 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public static Calendar[] getEnabledPeriod(String enabledPeriod) {
+ try {
+ if (enabledPeriod != null) {
+ enabledPeriod = enabledPeriod.trim();
+ if (enabledPeriod.startsWith(",")) {
+ return new Calendar[] { null, new ISO8601Date(enabledPeriod.substring(1)) };
+ } else if (enabledPeriod.endsWith(",")) {
+ return new Calendar[] {
+ new ISO8601Date(enabledPeriod.substring(0, enabledPeriod.length() - 1)),
+ null };
+ } else {
+ String[] period = StringUtils.split(enabledPeriod, ",");
+ return new Calendar[] { new ISO8601Date(period[0]), new ISO8601Date(period[1]) };
+ }
+ }
+ } catch (IllegalArgumentException e) {
+ LOGGER.debug("Invalid date specified ", e);
+ }
+ return new Calendar[] { null, null };
+ }
+
+ public static String getEnableValue(long from, long to, boolean day, TimeZone zone) {
+ StringBuilder sb = new StringBuilder();
+ if (from > 0) {
+ ISO8601Date before = new ISO8601Date();
+ before.setTimeInMillis(from);
+ before.setTimeZone(zone);
+ before.setDate(day);
+ sb.append(before.toString());
+ }
+ sb.append(",");
+ if (to > 0) {
+ ISO8601Date after = new ISO8601Date();
+ after.setTimeInMillis(to);
+ after.setTimeZone(zone);
+ after.setDate(day);
+ sb.append(after.toString());
+ }
+ if (sb.length() > 1) {
+ return sb.toString();
+ }
+ return null;
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/ISO8601Date.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/ISO8601Date.java
new file mode 100644
index 00000000..92fe6ea1
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/ISO8601Date.java
@@ -0,0 +1,200 @@
+package org.sakaiproject.nakamura.api.lite.util;
+
+import java.util.Calendar;
+import java.util.Formatter;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/**
+ *
+ */
+public class ISO8601Date extends GregorianCalendar {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 5115079662422026445L;
+ private boolean date;
+
+ /*
+ * 2010-03-17 Separate date and time in UTC: 2010-03-17 06:33Z Combined date
+ * and time in UTC: 2010-03-17T06:33Z
+ */
+ /**
+ *
+ */
+ public ISO8601Date() {
+ date = false;
+ }
+
+ public ISO8601Date(String spec) {
+ int l = spec.length();
+ int year = -1;
+ int month = -1;
+ int day = -1;
+ int hour = -1;
+ int min = -1;
+ int sec = -1;
+ TimeZone z = null;
+ date = false;
+ switch (l) {
+ case 16:// 19970714T170000Z
+ case 18:// 19970714T170000+01
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(4, 6));
+ day = Integer.parseInt(spec.substring(6, 8));
+ hour = Integer.parseInt(spec.substring(9, 11));
+ min = Integer.parseInt(spec.substring(11, 13));
+ sec = Integer.parseInt(spec.substring(13, 15));
+ if ('Z' == spec.charAt(l - 1)) {
+ z = TimeZone.getTimeZone("GMT");
+ } else {
+ z = TimeZone.getTimeZone("GMT" + spec.substring(15));
+ }
+ break;
+ case 20: // 1997-07-14T17:00:00Z // 19970714T170000+0100
+ if ('Z' == spec.charAt(l - 1)) {
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(5, 7));
+ day = Integer.parseInt(spec.substring(8, 10));
+ hour = Integer.parseInt(spec.substring(11, 13));
+ min = Integer.parseInt(spec.substring(14, 16));
+ sec = Integer.parseInt(spec.substring(17, 19));
+ z = TimeZone.getTimeZone("UTC");
+ } else {
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(4, 6));
+ day = Integer.parseInt(spec.substring(6, 8));
+ hour = Integer.parseInt(spec.substring(9, 11));
+ min = Integer.parseInt(spec.substring(11, 13));
+ sec = Integer.parseInt(spec.substring(13, 15));
+ z = TimeZone.getTimeZone("GMT" + spec.substring(15));
+ }
+ break;
+ case 22: // 1997-07-14T17:00:00+01
+ case 25: // 1997-07-14T17:00:00+01:00
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(5, 7));
+ day = Integer.parseInt(spec.substring(8, 10));
+ hour = Integer.parseInt(spec.substring(11, 13));
+ min = Integer.parseInt(spec.substring(14, 16));
+ sec = Integer.parseInt(spec.substring(17, 19));
+ z = TimeZone.getTimeZone("GMT" + spec.substring(19));
+ date = false;
+ break;
+ case 8: // 19970714
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(4, 6));
+ day = Integer.parseInt(spec.substring(6, 8));
+ hour = 0;
+ min = 0;
+ sec = 0;
+ z = TimeZone.getDefault(); // we really need to know the timezone of
+ // the user for
+ // this.
+ date = true;
+ break;
+ case 10: // 1997-07-14
+ year = Integer.parseInt(spec.substring(0, 4));
+ month = Integer.parseInt(spec.substring(5, 7));
+ day = Integer.parseInt(spec.substring(8, 10));
+ hour = 0;
+ min = 0;
+ sec = 0;
+ z = TimeZone.getDefault(); // we really need to know the timezone of
+ // the user for
+ // this.
+ date = true;
+ break;
+ default:
+ throw new IllegalArgumentException("Illeagal ISO8601 Date Time " + spec);
+ }
+ if (z == null) {
+ throw new IllegalArgumentException(
+ "Time Zone incorrectly formatted, must be one of Z or +00:00 or +0000. Time was "
+ + spec);
+ }
+ setTimeZone(z);
+ set(MILLISECOND, 0);
+ set(year, month - 1, day, hour, min, sec);
+ }
+
+ @Override
+ public int compareTo(Calendar anotherCalendar) {
+ if ( date ) {
+ int cmp = get(YEAR) - anotherCalendar.get(YEAR);
+ if ( cmp == 0 ) {
+ cmp = get(DAY_OF_YEAR) - anotherCalendar.get(DAY_OF_YEAR);
+ }
+ return cmp;
+ }
+ return super.compareTo(anotherCalendar);
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if ( obj instanceof ISO8601Date ) {
+ ISO8601Date d = (ISO8601Date) obj;
+ if ( date && d.date ) {
+ return get(YEAR) == d.get(YEAR) && get(DAY_OF_YEAR) == d.get(DAY_OF_YEAR);
+ } else if (date != d.date ) {
+ return false;
+ } else {
+ return super.equals(obj);
+ }
+ }
+ if ( date ) {
+ return false;
+ }
+ return super.equals(obj);
+ }
+
+ @Override
+ public String toString() {
+ Formatter formatter = new Formatter();
+ int year = get(YEAR);
+ int month = get(MONTH) + 1;
+ int day = get(DAY_OF_MONTH);
+ int hour = get(HOUR_OF_DAY);
+ int min = get(MINUTE);
+ int second = get(SECOND);
+ if (date) {
+ formatter.format("%04d-%02d-%02d", year, month, day);
+ } else {
+ // this prints out the offset, not the time zone name, but it takes
+ // into account DST if in effect for the time in question. Not that
+ // is
+ // not changed by the time of printing. This was checked on 23/11/2011.
+ long offset = getTimeZone().getOffset(getTimeInMillis()) / (60000L);
+ int hoffset = (int) (offset / 60L);
+ int minoffset = (int) (offset % 60L);
+ if (offset == 0) {
+ formatter.format("%04d-%02d-%02dT%02d:%02d:%02dZ", year, month, day, hour, min,
+ second);
+ } else if (offset < 0) {
+ formatter.format("%04d-%02d-%02dT%02d:%02d:%02d-%02d:%02d", year, month, day, hour,
+ min, second, -hoffset, -minoffset);
+ } else {
+ formatter.format("%04d-%02d-%02dT%02d:%02d:%02d+%02d:%02d", year, month, day, hour,
+ min, second, hoffset, minoffset);
+ }
+ }
+ return formatter.toString();
+ }
+
+ /**
+ * @param b
+ */
+ public void setDate(boolean b) {
+ date = b;
+ }
+
+ public boolean isDate() {
+ return date;
+ }
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/util/Iterables.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/Iterables.java
similarity index 100%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/util/Iterables.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/util/Iterables.java
diff --git a/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/PreemptiveIterator.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/PreemptiveIterator.java
new file mode 100644
index 00000000..dd716676
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/PreemptiveIterator.java
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.api.lite.util;
+
+import org.sakaiproject.nakamura.lite.storage.spi.CachableDisposableIterator;
+import org.sakaiproject.nakamura.lite.storage.spi.Disposer;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+/**
+ * A Iterator wrapper that pre-emptively checks the next value in the underlying iterator before responding true to hasNext().
+ * @param
+ */
+public abstract class PreemptiveIterator implements Iterator, CachableDisposableIterator {
+
+ private static final int UNDETERMINED = 0;
+ private static final int TRUE = 1;
+ private static final int FALSE = -1;
+ private int lastCheck = UNDETERMINED;
+ private Disposer disposer;
+
+ protected abstract boolean internalHasNext();
+
+ protected abstract T internalNext();
+
+ /**
+ * By default a preemptive iterator does not cache. Override this method to make it cache.
+ */
+ @Override
+ public Map getResultsMap() {
+ return null;
+ }
+
+ public final boolean hasNext() {
+ if (lastCheck == FALSE) {
+ return false;
+ }
+ if (lastCheck != UNDETERMINED) {
+ return (lastCheck == TRUE);
+ }
+ if (internalHasNext()) {
+ lastCheck = TRUE;
+ return true;
+ }
+ lastCheck = FALSE;
+ return false;
+ }
+
+ public final T next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ lastCheck = UNDETERMINED;
+ return internalNext();
+ }
+
+ public final void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void close() {
+ if ( disposer != null ) {
+ disposer.unregisterDisposable(this);
+ }
+ }
+
+ public void setDisposer(Disposer disposer) {
+ this.disposer = disposer;
+ }
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java
similarity index 94%
rename from src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java
rename to core/src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java
index 07008c0f..0476dfe5 100644
--- a/src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/api/lite/util/Type1UUID.java
@@ -1,20 +1,19 @@
/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
+ * regarding copyright ownership. The SF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
+ * with the License. You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
*/
package org.sakaiproject.nakamura.api.lite.util;
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java b/core/src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java
similarity index 61%
rename from src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java
index 118a20c8..4e25a65a 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/BaseMemoryRepository.java
@@ -1,23 +1,42 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
package org.sakaiproject.nakamura.lite;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import org.sakaiproject.nakamura.api.lite.ClientPoolException;
+import org.sakaiproject.nakamura.api.lite.Configuration;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.lite.authorizable.AuthorizableActivator;
-import org.sakaiproject.nakamura.lite.content.BlockContentHelper;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
-import org.sakaiproject.nakamura.lite.storage.StorageClientPool;
import org.sakaiproject.nakamura.lite.storage.mem.MemoryStorageClientPool;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClientPool;
+import org.sakaiproject.nakamura.lite.storage.spi.content.BlockContentHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.IOException;
import java.util.Map;
/**
- * Utiltiy class to create an entirely in memorty Sparse Repository, usefull for
+ * Utility class to create an entirely in memory Sparse Repository, useful for
* testing or bulk internal modifications.
*/
public class BaseMemoryRepository {
@@ -29,9 +48,7 @@ public class BaseMemoryRepository {
private RepositoryImpl repository;
public BaseMemoryRepository() throws StorageClientException, AccessDeniedException,
- ClientPoolException, ClassNotFoundException {
- clientPool = getClientPool();
- client = clientPool.getClient();
+ ClientPoolException, ClassNotFoundException, IOException {
configuration = new ConfigurationImpl();
Map properties = Maps.newHashMap();
properties.put("keyspace", "n");
@@ -39,6 +56,8 @@ public BaseMemoryRepository() throws StorageClientException, AccessDeniedExcepti
properties.put("authorizable-column-family", "au");
properties.put("content-column-family", "cn");
configuration.activate(properties);
+ clientPool = getClientPool(configuration);
+ client = clientPool.getClient();
AuthorizableActivator authorizableActivator = new AuthorizableActivator(client,
configuration);
authorizableActivator.setup();
@@ -56,10 +75,11 @@ public void close() {
client.close();
}
- protected StorageClientPool getClientPool() throws ClassNotFoundException {
+ protected StorageClientPool getClientPool(Configuration configuration) throws ClassNotFoundException {
MemoryStorageClientPool cp = new MemoryStorageClientPool();
cp.activate(ImmutableMap.of("test", (Object) "test",
- BlockContentHelper.CONFIG_MAX_CHUNKS_PER_BLOCK, 9));
+ BlockContentHelper.CONFIG_MAX_CHUNKS_PER_BLOCK, 9,
+ Configuration.class.getName(), configuration));
return cp;
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/CachingManagerImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/CachingManagerImpl.java
new file mode 100644
index 00000000..3476a00e
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/CachingManagerImpl.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite;
+
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+import org.sakaiproject.nakamura.lite.storage.spi.DirectCacheAccess;
+import org.sakaiproject.nakamura.lite.storage.spi.Disposable;
+import org.sakaiproject.nakamura.lite.storage.spi.Disposer;
+import org.sakaiproject.nakamura.lite.storage.spi.RowHasher;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.SecureRandom;
+import java.util.Map;
+
+/**
+ * Extend this class to add caching to a Manager class.
+ */
+public abstract class CachingManagerImpl implements DirectCacheAccess {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CachingManagerImpl.class);
+ private Map sharedCache;
+ private StorageClient client;
+ private long managerId;
+ private static SecureRandom secureRandom = new SecureRandom(); // need to assume that the secure random will be reasonably quick to start up
+
+ /**
+ * Create a new {@link CachingManagerImpl}
+ * @param client a client to the underlying storage engine
+ * @param sharedCache the cache where the objects will be stored
+ */
+ public CachingManagerImpl(StorageClient client, Map sharedCache) {
+ this.client = client;
+ this.sharedCache = sharedCache;
+ managerId = getManagerId();
+ }
+
+ private long getManagerId() {
+ // needs to have a low probability of clashing with any other Cache manager in the cluster.
+ // no idea what the probability of a clash is here, although I assume its lowish.
+ return secureRandom.nextLong();
+ }
+
+ /**
+ * Try to retrieve an object from the cache.
+ * Has the side-effect of loading an uncached object into cache the first time.
+ * @param keySpace the key space we're operating in.
+ * @param columnFamily the column family for the object
+ * @param key the object key
+ * @return the object or null if not cached and not found
+ * @throws StorageClientException
+ */
+ protected Map getCached(String keySpace, String columnFamily, String key)
+ throws StorageClientException {
+ Map m = null;
+ String cacheKey = getCacheKey(keySpace, columnFamily, key);
+
+ CacheHolder cacheHolder = getFromCacheInternal(cacheKey);
+ if (cacheHolder != null ) {
+ m = cacheHolder.get();
+ if ( m != null ) {
+ LOGGER.debug("Cache Hit {} {} {} ", new Object[] { cacheKey, cacheHolder, m });
+ }
+ }
+ if (m == null) {
+ m = client.get(keySpace, columnFamily, key);
+ if (m != null) {
+ LOGGER.debug("Cache Miss, Found Map {} {}", cacheKey, m);
+ }
+ putToCacheInternal(cacheKey, new CacheHolder(m), true);
+ }
+ return m;
+ }
+ public void putToCache(String cacheKey, CacheHolder cacheHolder) {
+ putToCache(cacheKey, cacheHolder, false);
+ }
+
+ public void putToCache(String cacheKey, CacheHolder cacheHolder, boolean respectDeletes) {
+ if ( client instanceof RowHasher ) {
+ putToCacheInternal(cacheKey, cacheHolder, respectDeletes);
+ }
+ }
+
+ private void putToCacheInternal(String cacheKey, CacheHolder cacheHolder, boolean respectDeletes) {
+ if (sharedCache != null) {
+ if ( respectDeletes ) {
+ CacheHolder ch = sharedCache.get(cacheKey);
+ if ( ch != null && ch.get() == null ) {
+ // item is deleted, dont update it
+ return;
+ }
+ }
+ sharedCache.put(cacheKey, cacheHolder);
+ }
+ }
+ public CacheHolder getFromCache(String cacheKey) {
+ if ( client instanceof RowHasher ) {
+ return getFromCacheInternal(cacheKey);
+ }
+ return null;
+ }
+ private CacheHolder getFromCacheInternal(String cacheKey) {
+ if (sharedCache != null && sharedCache.containsKey(cacheKey)) {
+ return sharedCache.get(cacheKey);
+ }
+ return null;
+ }
+
+ protected abstract Logger getLogger();
+
+ /**
+ * Combine the parameters into a key suitable for storage and lookup in the cache.
+ * @param keySpace
+ * @param columnFamily
+ * @param key
+ * @return the cache key
+ * @throws StorageClientException
+ */
+ private String getCacheKey(String keySpace, String columnFamily, String key) throws StorageClientException {
+ if ( client instanceof RowHasher) {
+ return ((RowHasher) client).rowHash(keySpace, columnFamily, key);
+ }
+ return keySpace + ":" + columnFamily + ":" + key;
+ }
+
+ /**
+ * Remove this object from the cache. Note, StorageClient uses the word
+ * remove to mean delete. This method should do the same.
+ *
+ * @param keySpace
+ * @param columnFamily
+ * @param key
+ * @throws StorageClientException
+ */
+ protected void removeCached(String keySpace, String columnFamily, String key) throws StorageClientException {
+ if (sharedCache != null) {
+ // insert a replacement. This should cause an invalidation message to propagate in the cluster.
+ final String cacheKey = getCacheKey(keySpace, columnFamily, key);
+ putToCacheInternal(cacheKey, new CacheHolder(null, managerId), false);
+ LOGGER.debug("Marked as deleted in Cache {} ", cacheKey);
+ if ( client instanceof Disposer ) {
+ // we might want to change this to register the action as a commit handler rather than a disposable.
+ // it depends on if we think the delete is a transactional thing or a operational cache thing.
+ // at the moment, I am leaning towards an operational cache thing, since regardless of if
+ // the session commits or not, we want this to dispose when the session is closed, or commits.
+ ((Disposer)client).registerDisposable(new Disposable() {
+
+ @Override
+ public void setDisposer(Disposer disposer) {
+ }
+
+ @Override
+ public void close() {
+ CacheHolder ch = sharedCache.get(cacheKey);
+ if ( ch != null && ch.wasLockedTo(managerId)) {
+ sharedCache.remove(cacheKey);
+ LOGGER.debug("Removed deleted marker from Cache {} ", cacheKey);
+ }
+ }
+ });
+ }
+ }
+ client.remove(keySpace, columnFamily, key);
+
+ }
+
+ /**
+ * Put an object in the cache
+ * @param keySpace
+ * @param columnFamily
+ * @param key
+ * @param encodedProperties the object to be stored
+ * @param probablyNew whether or not this object is new.
+ * @throws StorageClientException
+ */
+ protected void putCached(String keySpace, String columnFamily, String key,
+ Map encodedProperties, boolean probablyNew)
+ throws StorageClientException {
+ String cacheKey = null;
+ if ( sharedCache != null ) {
+ cacheKey = getCacheKey(keySpace, columnFamily, key);
+ }
+ if ( sharedCache != null && !probablyNew ) {
+ CacheHolder ch = getFromCacheInternal(cacheKey);
+ if ( ch != null && ch.isLocked(this.managerId) ) {
+ LOGGER.debug("Is Locked {} ",ch);
+ return; // catch the case where another method creates while something is in the cache.
+ // this is a big assumption since if the item is not in the cache it will get updated
+ // there is no difference in sparsemap between create and update, they are all insert operations
+ // what we are really saying here is that inorder to update the item you have to have just got it
+ // and if you failed to get it, your update must have been a create operation. As long as the dwell time
+ // in the cache is longer than the lifetime of an active session then this will be true.
+ // if the lifetime of an active session is longer (like with a long running background operation)
+ // then you should expect to see race conditions at this point since the marker in the cache will have
+ // gone, and the marker in the database has gone, so the put operation, must be a create operation.
+ // To change this behavior we would need to differentiate more strongly between new and update and change
+ // probablyNew into certainlyNew, but that would probably break the BASIC assumption of the whole system.
+ // Update 2011-12-06 related to issue 136
+ // I am not certain this code is correct. What happens if the session wants to remove and then add items.
+ // the session will never get past this point, since sitting in the cache is a null CacheHolder preventing the session
+ // removing then adding.
+ // also, how long should the null cache holder be placed in there for ?
+ // I think the solution is to bind the null Cache holder to the instance of the caching manager that created it,
+ // let the null Cache holder last for 10s, and during that time only the CachingManagerImpl that created it can remove it.
+ }
+ }
+ LOGGER.debug("Saving {} {} {} {} ", new Object[] { keySpace, columnFamily, key,
+ encodedProperties });
+ client.insert(keySpace, columnFamily, key, encodedProperties, probablyNew);
+ if ( sharedCache != null ) {
+ // if we just added a value in, remove the key so that any stale state (including a previously deleted object is removed)
+ sharedCache.remove(cacheKey);
+ }
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/ConfigurationImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/ConfigurationImpl.java
new file mode 100644
index 00000000..59ba4b7b
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/ConfigurationImpl.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.felix.scr.annotations.Service;
+import org.sakaiproject.nakamura.api.lite.Configuration;
+import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+@Component(immediate = true, metatype = true)
+@Service(value = Configuration.class)
+public class ConfigurationImpl implements Configuration {
+
+ @Property(value = "ac")
+ protected static final String ACL_COLUMN_FAMILY = "acl-column-family";
+ @Property(value = "n")
+ protected static final String KEYSPACE = "keyspace";
+ @Property(value = "au")
+ protected static final String AUTHORIZABLE_COLUMN_FAMILY = "authorizable-column-family";
+ @Property(value = "cn")
+ protected static final String CONTENT_COLUMN_FAMILY = "content-column-family";
+ @Property(value = "lk")
+ protected static final String LOCK_COLUMN_FAMILY = "lock-column-family";
+
+ protected static final String DEFAULT_INDEX_COLUMN_NAMES = "au:rep:principalName,au:type,cn:sling:resourceType," +
+ "cn:sakai:pooled-content-manager,cn:sakai:messagestore,cn:sakai:type,cn:sakai:marker,cn:sakai:tag-uuid," +
+ "cn:sakai:contactstorepath,cn:sakai:state,cn:_created,cn:sakai:category,cn:sakai:messagebox,cn:sakai:from," +
+ "cn:sakai:subject";
+
+ @Property(value=DEFAULT_INDEX_COLUMN_NAMES)
+ protected static final String INDEX_COLUMN_NAMES = "index-column-names";
+
+ private static final String DEFAULT_INDEX_COLUMN_TYPES = "cn:sakai:pooled-content-manager=String[],cn:sakai:category=String[]";
+
+ @Property(value=DEFAULT_INDEX_COLUMN_TYPES)
+ protected static final String INDEX_COLUMN_TYPES = "index-column-types";
+
+
+ private static final String SHAREDCONFIGPATH = "org/sakaiproject/nakamura/lite/shared.properties";
+
+ protected static final String SHAREDCONFIGPROPERTY = "sparseconfig";
+ private static final Logger LOGGER = LoggerFactory.getLogger(ConfigurationImpl.class);
+
+
+ private String aclColumnFamily;
+ private String keySpace;
+ private String authorizableColumnFamily;
+ private String contentColumnFamily;
+ private String lockColumnFamily;
+ private String[] indexColumnNames;
+ private Map sharedProperties;
+ private String[] indexColumnTypes;
+
+ @SuppressWarnings("unchecked")
+ @Activate
+ public void activate(Map properties) throws IOException {
+ aclColumnFamily = StorageClientUtils.getSetting(properties.get(ACL_COLUMN_FAMILY), "ac");
+ keySpace = StorageClientUtils.getSetting(properties.get(KEYSPACE), "n");
+ authorizableColumnFamily = StorageClientUtils.getSetting(properties.get(AUTHORIZABLE_COLUMN_FAMILY), "au");
+ contentColumnFamily = StorageClientUtils.getSetting(properties.get(CONTENT_COLUMN_FAMILY), "cn");
+ lockColumnFamily = StorageClientUtils.getSetting(properties.get(LOCK_COLUMN_FAMILY), "ln");
+
+ // load defaults
+ // check the classpath
+ sharedProperties = Maps.newHashMap();
+ InputStream in = this.getClass().getClassLoader().getResourceAsStream(SHAREDCONFIGPATH);
+ if ( in != null ) {
+ Properties p = new Properties();
+ p.load(in);
+ in.close();
+ sharedProperties.putAll(Maps.fromProperties(p));
+ }
+ // Load from a properties file defiend on the command line
+ String osSharedConfigPath = System.getProperty(SHAREDCONFIGPROPERTY);
+ if ( osSharedConfigPath != null && StringUtils.isNotEmpty(osSharedConfigPath)) {
+ File f = new File(osSharedConfigPath);
+ if ( f.exists() && f.canRead() ) {
+ FileReader fr = new FileReader(f);
+ Properties p = new Properties();
+ p.load(fr);
+ fr.close();
+ sharedProperties.putAll(Maps.fromProperties(p));
+ } else {
+ LOGGER.warn("Unable to read shared config file {} specified by the system property {} ",f.getAbsolutePath(), SHAREDCONFIGPROPERTY);
+ }
+ }
+
+ // make the shared properties immutable.
+ sharedProperties = ImmutableMap.copyOf(sharedProperties);
+ indexColumnNames = StringUtils.split(getProperty(INDEX_COLUMN_NAMES,DEFAULT_INDEX_COLUMN_NAMES, sharedProperties, properties),',');
+ LOGGER.info("Using Configuration for Index Column Names as {}", Arrays.toString(indexColumnNames));
+ indexColumnTypes = StringUtils.split(getProperty(INDEX_COLUMN_TYPES,DEFAULT_INDEX_COLUMN_TYPES, sharedProperties, properties),',');
+
+
+
+
+ }
+
+ private String getProperty(String name, String defaultValue,
+ Map ...properties ) {
+ // if present in the shared properties, load the default from there.
+ String value = defaultValue;
+ for ( Map p : properties ) {
+ if ( p.containsKey(name) ) {
+ Object v = p.get(name);
+ if ( v != null && !defaultValue.equals(v)) {
+ value = String.valueOf(v);
+ LOGGER.debug("{} is configured as {}", value);
+ }
+ }
+ }
+ return value;
+
+ }
+
+ public String getAclColumnFamily() {
+ return aclColumnFamily;
+ }
+
+ public String getKeySpace() {
+ return keySpace;
+ }
+
+ public String getAuthorizableColumnFamily() {
+ return authorizableColumnFamily;
+ }
+
+ public String getContentColumnFamily() {
+ return contentColumnFamily;
+ }
+
+ public String getLockColumnFamily() {
+ return lockColumnFamily;
+ }
+
+ public String[] getIndexColumnNames() {
+ return indexColumnNames;
+ }
+ public Map getSharedConfig() {
+ return sharedProperties;
+ }
+
+ public String[] getIndexColumnTypes() {
+ return indexColumnTypes;
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/LoggingStorageListener.java b/core/src/main/java/org/sakaiproject/nakamura/lite/LoggingStorageListener.java
new file mode 100644
index 00000000..8f255eb3
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/LoggingStorageListener.java
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite;
+
+import org.sakaiproject.nakamura.api.lite.StoreListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.Map;
+
+public class LoggingStorageListener implements StoreListener {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(LoggingStorageListener.class);
+ private boolean quiet;
+
+ public LoggingStorageListener(boolean quiet) {
+ this.quiet = quiet;
+ }
+
+ public LoggingStorageListener() {
+ this.quiet = false;
+ }
+
+ public void onDelete(String zone, String path, String user, String resourceType, Map beforeEvent,
+ String... attributes) {
+ if (!quiet) {
+ LOGGER.info("Delete {} {} {} {} {} ",
+ new Object[] { zone, path, user, resourceType, Arrays.toString(attributes) });
+ }
+ }
+
+ public void onUpdate(String zone, String path, String user, String resourceType, boolean isNew,
+ Map beforeEvent, String... attributes) {
+ if (!quiet) {
+ LOGGER.info("Update {} {} {} {} new:{} {} ", new Object[] { zone, path, user, resourceType, isNew,
+ Arrays.toString(attributes) });
+ }
+ }
+
+ public void onLogin(String userId, String sessionId) {
+ if (!quiet) {
+ LOGGER.info("Login {} {}", new Object[] { userId, sessionId });
+ }
+ }
+
+ public void onLogout(String userId, String sessionId) {
+ if (!quiet) {
+ LOGGER.info("Logout {} {}", new Object[] { userId, sessionId });
+ }
+ }
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/storage/DisposableIterator.java b/core/src/main/java/org/sakaiproject/nakamura/lite/ManualOperationService.java
similarity index 78%
rename from src/main/java/org/sakaiproject/nakamura/lite/storage/DisposableIterator.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/ManualOperationService.java
index 8b7dbf22..fc615920 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/storage/DisposableIterator.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/ManualOperationService.java
@@ -1,4 +1,4 @@
-/*
+/**
* Licensed to the Sakai Foundation (SF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -15,16 +15,15 @@
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
-package org.sakaiproject.nakamura.lite.storage;
-
-import java.util.Iterator;
+package org.sakaiproject.nakamura.lite;
/**
- * Disposable Iterators must be closed when they have been used.
+ * A marker service for components that are disabled and perform an action on
+ * activation. (OSGi component validation requirement)
*
* @author ieb
*
- * @param
*/
-public interface DisposableIterator extends Iterator, Disposable {
+public interface ManualOperationService {
+
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/NullCacheManagerX.java b/core/src/main/java/org/sakaiproject/nakamura/lite/NullCacheManagerX.java
new file mode 100644
index 00000000..783a8c4d
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/NullCacheManagerX.java
@@ -0,0 +1,40 @@
+package org.sakaiproject.nakamura.lite;
+
+import java.util.Map;
+
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
+import org.sakaiproject.nakamura.api.lite.StorageCacheManager;
+
+/**
+ * Unmanaged Caches are used where there is nothing else provided by the client.
+ * @author ieb
+ *
+ */
+public class NullCacheManagerX implements StorageCacheManager {
+
+
+
+ @Override
+ public Map getAccessControlCache() {
+ return null;
+ }
+
+ @Override
+ public Map getAuthorizableCache() {
+ return null;
+ }
+
+ @Override
+ public Map getContentCache() {
+ return null;
+ }
+
+ @Override
+ public Map getCache(String cacheName) {
+ return null;
+ }
+
+
+
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java b/core/src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java
similarity index 73%
rename from src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java
index 0636c41f..29807da6 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/OSGiStoreListener.java
@@ -1,3 +1,20 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
package org.sakaiproject.nakamura.lite;
import com.google.common.collect.ImmutableMap;
@@ -18,6 +35,10 @@
import java.util.Hashtable;
import java.util.Map;
+/**
+ * When this {@link StoreListener} is notified of a storage action
+ * it posts an OSGi {@link Event} to the {@link EventAdmin}
+ */
@Component(immediate = true, metatype = true)
@Service
public class OSGiStoreListener implements StoreListener {
@@ -60,15 +81,21 @@ public class OSGiStoreListener implements StoreListener {
}
- public void onDelete(String zone, String path, String user, String... attributes) {
+ /**
+ * {@inheritDoc}
+ */
+ public void onDelete(String zone, String path, String user, String resourceType, Map beforeEvent, String ... attributes) {
String topic = DEFAULT_DELETE_TOPIC;
if (deleteTopics.containsKey(zone)) {
topic = deleteTopics.get(zone);
}
- postEvent(topic, path, user, attributes);
+ postEvent(topic, path, user, resourceType, beforeEvent, attributes);
}
- public void onUpdate(String zone, String path, String user, boolean isNew, String... attributes) {
+ /**
+ * {@inheritDoc}
+ */
+ public void onUpdate(String zone, String path, String user, String resourceType, boolean isNew, Map beforeEvent, String... attributes) {
String topic = DEFAULT_UPDATE_TOPIC;
if (isNew) {
@@ -82,19 +109,29 @@ public void onUpdate(String zone, String path, String user, boolean isNew, Strin
}
}
- postEvent(topic, path, user, attributes);
+ postEvent(topic, path, user, resourceType, beforeEvent, attributes);
}
+ /**
+ * {@inheritDoc}
+ *
+ * No event is posted for these actions.
+ */
public void onLogin(String userid, String sessionID) {
LOGGER.debug("Login {} {} ", userid, sessionID);
}
+ /**
+ * {@inheritDoc}
+ *
+ * No event is posted for these actions.
+ */
public void onLogout(String userid, String sessionID) {
LOGGER.debug("Logout {} {} ", userid, sessionID);
}
- private void postEvent(String topic, String path, String user, String[] attributes) {
- final Dictionary properties = new Hashtable();
+ private void postEvent(String topic, String path, String user, String resourceType, Map beforeEvent, String[] attributes) {
+ final Dictionary properties = new Hashtable();
if (attributes != null) {
for (String attribute : attributes) {
String[] parts = StringUtils.split(attribute, ":", 2);
@@ -110,7 +147,13 @@ private void postEvent(String topic, String path, String user, String[] attribut
if (path != null) {
properties.put(PATH_PROPERTY, path);
}
+ if ( resourceType != null ) {
+ properties.put(RESOURCE_TYPE_PROPERTY, resourceType);
+ }
properties.put(USERID_PROPERTY, user);
+ if ( beforeEvent != null) {
+ properties.put(BEFORE_EVENT_PROPERTY, beforeEvent);
+ }
eventAdmin.postEvent(new Event(topic, properties));
}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java
similarity index 65%
rename from src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java
index beef8a03..d62bc88b 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/RepositoryImpl.java
@@ -22,18 +22,23 @@
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
import org.sakaiproject.nakamura.api.lite.ClientPoolException;
import org.sakaiproject.nakamura.api.lite.Configuration;
import org.sakaiproject.nakamura.api.lite.Repository;
import org.sakaiproject.nakamura.api.lite.Session;
+import org.sakaiproject.nakamura.api.lite.StorageCacheManager;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.StoreListener;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
import org.sakaiproject.nakamura.lite.accesscontrol.AuthenticatorImpl;
import org.sakaiproject.nakamura.lite.authorizable.AuthorizableActivator;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
-import org.sakaiproject.nakamura.lite.storage.StorageClientPool;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClientPool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.Map;
@@ -41,19 +46,30 @@
@Service(value = Repository.class)
public class RepositoryImpl implements Repository {
+ private static final Logger LOGGER = LoggerFactory.getLogger(RepositoryImpl.class);
+
@Reference
protected Configuration configuration;
@Reference
protected StorageClientPool clientPool;
-
- @Reference
+
+ @Reference
protected StoreListener storeListener;
+ @Reference
+ protected PrincipalValidatorResolver principalValidatorResolver;
public RepositoryImpl() {
}
+ public RepositoryImpl(Configuration configuration, StorageClientPool clientPool,
+ LoggingStorageListener listener) {
+ this.configuration = configuration;
+ this.clientPool = clientPool;
+ this.storeListener = listener;
+ }
+
@Activate
public void activate(Map properties) throws ClientPoolException,
StorageClientException, AccessDeniedException {
@@ -64,8 +80,11 @@ public void activate(Map properties) throws ClientPoolException,
configuration);
authorizableActivator.setup();
} finally {
- client.close();
- clientPool.getClient();
+ if (client != null) {
+ client.close();
+ } else {
+ LOGGER.error("Failed to actvate repository, probably failed to create default users");
+ }
}
}
@@ -93,17 +112,23 @@ public Session loginAdministrative(String username) throws StorageClientExceptio
return openSession(username);
}
+ public Session loginAdministrativeBypassEnable(String username) throws StorageClientException,
+ ClientPoolException, AccessDeniedException {
+ return openSessionBypassEnable(username);
+ }
+
private Session openSession(String username, String password) throws StorageClientException,
AccessDeniedException {
StorageClient client = null;
try {
client = clientPool.getClient();
- AuthenticatorImpl authenticatorImpl = new AuthenticatorImpl(client, configuration);
+ AuthenticatorImpl authenticatorImpl = new AuthenticatorImpl(client, configuration, getAuthorizableCache(clientPool.getStorageCacheManager()));
User currentUser = authenticatorImpl.authenticate(username, password);
if (currentUser == null) {
throw new StorageClientException("User " + username + " cant login with password");
}
- return new SessionImpl(this, currentUser, client, configuration, clientPool.getStorageCacheManager(), storeListener);
+ return new SessionImpl(this, currentUser, client, configuration,
+ clientPool.getStorageCacheManager(), storeListener, principalValidatorResolver);
} catch (ClientPoolException e) {
clientPool.getClient();
throw e;
@@ -119,18 +144,54 @@ private Session openSession(String username, String password) throws StorageClie
}
}
+ private Map getAuthorizableCache(StorageCacheManager storageCacheManager) {
+ if ( storageCacheManager != null ) {
+ return storageCacheManager.getAuthorizableCache();
+ }
+ return null;
+ }
+
private Session openSession(String username) throws StorageClientException,
AccessDeniedException {
StorageClient client = null;
try {
client = clientPool.getClient();
- AuthenticatorImpl authenticatorImpl = new AuthenticatorImpl(client, configuration);
+ AuthenticatorImpl authenticatorImpl = new AuthenticatorImpl(client, configuration, getAuthorizableCache(clientPool.getStorageCacheManager()));
User currentUser = authenticatorImpl.systemAuthenticate(username);
if (currentUser == null) {
throw new StorageClientException("User " + username
+ " does not exist, cant login administratively as this user");
}
- return new SessionImpl(this, currentUser, client, configuration, clientPool.getStorageCacheManager(), storeListener);
+ return new SessionImpl(this, currentUser, client, configuration,
+ clientPool.getStorageCacheManager(), storeListener, principalValidatorResolver);
+ } catch (ClientPoolException e) {
+ clientPool.getClient();
+ throw e;
+ } catch (StorageClientException e) {
+ clientPool.getClient();
+ throw e;
+ } catch (AccessDeniedException e) {
+ clientPool.getClient();
+ throw e;
+ } catch (Throwable e) {
+ clientPool.getClient();
+ throw new StorageClientException(e.getMessage(), e);
+ }
+ }
+
+ private Session openSessionBypassEnable(String username) throws StorageClientException,
+ AccessDeniedException {
+ StorageClient client = null;
+ try {
+ client = clientPool.getClient();
+ AuthenticatorImpl authenticatorImpl = new AuthenticatorImpl(client, configuration, getAuthorizableCache(clientPool.getStorageCacheManager()));
+ User currentUser = authenticatorImpl.systemAuthenticateBypassEnable(username);
+ if (currentUser == null) {
+ throw new StorageClientException("User " + username
+ + " does not exist, cant login administratively as this user");
+ }
+ return new SessionImpl(this, currentUser, client, configuration,
+ clientPool.getStorageCacheManager(), storeListener, principalValidatorResolver);
} catch (ClientPoolException e) {
clientPool.getClient();
throw e;
@@ -156,7 +217,7 @@ public void setConnectionPool(StorageClientPool connectionPool) {
public void setStorageListener(StoreListener storeListener) {
this.storeListener = storeListener;
-
+
}
}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java
similarity index 57%
rename from src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java
index 127b1ca6..6edd2f16 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/SessionImpl.java
@@ -17,7 +17,11 @@
*/
package org.sakaiproject.nakamura.lite;
+import java.util.Map;
+
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
import org.sakaiproject.nakamura.api.lite.ClientPoolException;
+import org.sakaiproject.nakamura.api.lite.CommitHandler;
import org.sakaiproject.nakamura.api.lite.Configuration;
import org.sakaiproject.nakamura.api.lite.Repository;
import org.sakaiproject.nakamura.api.lite.Session;
@@ -26,48 +30,80 @@
import org.sakaiproject.nakamura.api.lite.StoreListener;
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.accesscontrol.Authenticator;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
import org.sakaiproject.nakamura.lite.accesscontrol.AccessControlManagerImpl;
import org.sakaiproject.nakamura.lite.accesscontrol.AuthenticatorImpl;
import org.sakaiproject.nakamura.lite.authorizable.AuthorizableManagerImpl;
import org.sakaiproject.nakamura.lite.content.ContentManagerImpl;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
+import org.sakaiproject.nakamura.lite.lock.LockManagerImpl;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.Maps;
public class SessionImpl implements Session {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SessionImpl.class);
private AccessControlManagerImpl accessControlManager;
private ContentManagerImpl contentManager;
private AuthorizableManagerImpl authorizableManager;
+ private LockManagerImpl lockManager;
private User currentUser;
private Repository repository;
private Exception closedAt;
private StorageClient client;
private Authenticator authenticator;
private StoreListener storeListener;
+ private Map commitHandlers = Maps.newLinkedHashMap();
+ private StorageCacheManager storageCacheManager;
+ private Configuration configuration;
+ private static long nagclient;
public SessionImpl(Repository repository, User currentUser, StorageClient client,
- Configuration configuration, StorageCacheManager storageCacheManager, StoreListener storeListener)
+ Configuration configuration, StorageCacheManager storageCacheManager,
+ StoreListener storeListener, PrincipalValidatorResolver principalValidatorResolver)
throws ClientPoolException, StorageClientException, AccessDeniedException {
this.currentUser = currentUser;
this.repository = repository;
this.client = client;
+ this.storageCacheManager = storageCacheManager;
+ this.storeListener = storeListener;
+ this.configuration = configuration;
+
+ if ( this.storageCacheManager == null ) {
+ if ( (nagclient % 1000) == 0 ) {
+ LOGGER.warn("No Cache Manager, All Caching disabled, please provide an Implementation of NamedCacheManager. This message will appear every 1000th time a session is created. ");
+ }
+ nagclient++;
+ }
accessControlManager = new AccessControlManagerImpl(client, currentUser, configuration,
- storageCacheManager.getAccessControlCache(), storeListener);
- authorizableManager = new AuthorizableManagerImpl(currentUser, client, configuration,
- accessControlManager, storageCacheManager.getAuthorizableCache(), storeListener);
+ getCache(configuration.getAclColumnFamily()), storeListener,
+ principalValidatorResolver);
+ Map authorizableCache = getCache(configuration
+ .getAuthorizableColumnFamily());
+ authorizableManager = new AuthorizableManagerImpl(currentUser, this, client, configuration,
+ accessControlManager, authorizableCache, storeListener);
- contentManager = new ContentManagerImpl(client, accessControlManager, configuration, storageCacheManager.getContentCache(), storeListener);
+ contentManager = new ContentManagerImpl(client, accessControlManager, configuration,
+ getCache(configuration.getContentColumnFamily()), storeListener);
+
+ lockManager = new LockManagerImpl(client, configuration, currentUser,
+ getCache(configuration.getLockColumnFamily()));
+
+ authenticator = new AuthenticatorImpl(client, configuration, authorizableCache);
- authenticator = new AuthenticatorImpl(client, configuration);
- this.storeListener = storeListener;
storeListener.onLogin(currentUser.getId(), this.toString());
}
public void logout() throws ClientPoolException {
if (closedAt == null) {
+ commit();
accessControlManager.close();
authorizableManager.close();
contentManager.close();
+ lockManager.close();
client.close();
accessControlManager = null;
authorizableManager = null;
@@ -94,6 +130,11 @@ public ContentManagerImpl getContentManager() throws StorageClientException {
return contentManager;
}
+ public LockManagerImpl getLockManager() throws StorageClientException {
+ check();
+ return lockManager;
+ }
+
public Authenticator getAuthenticator() throws StorageClientException {
check();
return authenticator;
@@ -114,4 +155,39 @@ private void check() throws StorageClientException {
}
}
+ public StorageClient getClient() {
+ return client;
+ }
+
+ public void addCommitHandler(String key, CommitHandler commitHandler) {
+ synchronized (commitHandlers) {
+ commitHandlers.put(key, commitHandler);
+ }
+ }
+
+ public void commit() {
+ synchronized (commitHandlers) {
+ for (CommitHandler commitHandler : commitHandlers.values()) {
+ commitHandler.commit();
+ }
+ commitHandlers.clear();
+ }
+ }
+
+ public Map getCache(String columnFamily) {
+ if (storageCacheManager != null) {
+ if (configuration.getAuthorizableColumnFamily().equals(columnFamily)) {
+ return storageCacheManager.getAuthorizableCache();
+ }
+ if (configuration.getAclColumnFamily().equals(columnFamily)) {
+ return storageCacheManager.getAccessControlCache();
+ }
+ if (configuration.getContentColumnFamily().equals(columnFamily)) {
+ return storageCacheManager.getContentCache();
+ }
+ return storageCacheManager.getCache(columnFamily);
+ }
+ return null;
+ }
+
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlManagerImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlManagerImpl.java
new file mode 100644
index 00000000..d4ebedd4
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlManagerImpl.java
@@ -0,0 +1,731 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.StringUtils;
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
+import org.sakaiproject.nakamura.api.lite.Configuration;
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
+import org.sakaiproject.nakamura.api.lite.StoreListener;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessControlManager;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AclModification;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.Permission;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.Permissions;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalTokenResolver;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.Security;
+import org.sakaiproject.nakamura.api.lite.authorizable.Authorizable;
+import org.sakaiproject.nakamura.api.lite.authorizable.AuthorizableManager;
+import org.sakaiproject.nakamura.api.lite.authorizable.Group;
+import org.sakaiproject.nakamura.api.lite.authorizable.User;
+import org.sakaiproject.nakamura.api.lite.content.Content;
+import org.sakaiproject.nakamura.lite.CachingManagerImpl;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import edu.umd.cs.findbugs.annotations.SuppressWarnings;
+
+public class AccessControlManagerImpl extends CachingManagerImpl implements AccessControlManager {
+
+ private static final String _SECRET_KEY = "_secretKey";
+ private static final String _PATH = "_aclPath";
+ private static final String _OBJECT_TYPE = "_aclType";
+ public static final String _KEY = "_aclKey";
+ private static final Logger LOGGER = LoggerFactory.getLogger(AccessControlManagerImpl.class);
+ private static final Set PROTECTED_PROPERTIES = ImmutableSet.of(_SECRET_KEY);
+ private static final Set READ_ONLY_PROPERTIES = ImmutableSet.of(_SECRET_KEY, _PATH, _OBJECT_TYPE, _KEY);
+ private User user;
+ private String keySpace;
+ private String aclColumnFamily;
+ private Map cache = new ConcurrentHashMap();
+ private boolean closed;
+ private StoreListener storeListener;
+ private PrincipalTokenValidator principalTokenValidator;
+ private PrincipalTokenResolver principalTokenResolver;
+ private SecureRandom secureRandom;
+ private AuthorizableManager authorizableManager;
+ private Map principalCache = new ConcurrentHashMap();
+ private ThreadLocal principalRecursionLock = new ThreadLocal();
+ private ThreadBoundStackReferenceCounter compilingPermissions = new ThreadBoundStackReferenceCounter();
+
+ public AccessControlManagerImpl(StorageClient client, User currentUser, Configuration config,
+ Map sharedCache, StoreListener storeListener, PrincipalValidatorResolver principalValidatorResolver) throws StorageClientException {
+ super(client, sharedCache);
+ this.user = currentUser;
+ this.aclColumnFamily = config.getAclColumnFamily();
+ this.keySpace = config.getKeySpace();
+ closed = false;
+ this.storeListener = storeListener;
+ principalTokenValidator = new PrincipalTokenValidator(principalValidatorResolver);
+ secureRandom = new SecureRandom();
+ }
+
+ public Map getAcl(String objectType, String objectPath)
+ throws StorageClientException, AccessDeniedException {
+ checkOpen();
+ check(objectType, objectPath, Permissions.CAN_READ_ACL);
+
+ String key = this.getAclKey(objectType, objectPath);
+ return StorageClientUtils.getFilterMap(getCached(keySpace, aclColumnFamily, key), null, null, PROTECTED_PROPERTIES, false);
+ }
+
+ /**
+ * Property principals are stored with keys of the form
+ * _pp_@@ where principal is a principal. For the
+ * acl to be selected for the used of this session, they must have that
+ * principal. property is the name of the property. g or d is grant or deny.
+ * The value of the ACE is the bitmap for the ACE. All ACEs are selected and
+ * processed to form the ACE for the user, returned as a PropertyAcl.
+ */
+ public PropertyAcl getPropertyAcl(String objectType, String objectPath) throws AccessDeniedException, StorageClientException {
+ checkOpen();
+ compilingPermissions.inc();
+ try {
+ String key = this.getAclKey(objectType, objectPath);
+ Map objectAcl = getCached(keySpace, aclColumnFamily, key);
+ Set orderedPrincipals = Sets.newLinkedHashSet();
+ {
+ String principal = user.getId();
+ if ( principal.startsWith("_") ) {
+ throw new StorageClientException("Princials may not start with _ ");
+ }
+ orderedPrincipals.add(principal);
+ }
+ for (String principal : getPrincipals(user) ) {
+ if ( principal.startsWith("_") ) {
+ throw new StorageClientException("Princials may not start with _ ");
+ }
+ orderedPrincipals.add(principal);
+ }
+ // Everyone must be the last principal to be applied
+ if (!User.ANON_USER.equals(user.getId())) {
+ orderedPrincipals.add(Group.EVERYONE);
+ }
+ // go through each principal
+ Map grants = Maps.newHashMap();
+ Map denies = Maps.newHashMap();
+ for ( String principal : orderedPrincipals) {
+ // got through each property
+ String ppk = PROPERTY_PRINCIPAL_STEM+principal;
+ for(Entry e : objectAcl.entrySet()) {
+ String k = e.getKey();
+ if ( k.startsWith(ppk)) {
+ String[] parts = StringUtils.split(k.substring(PROPERTY_PRINCIPAL_STEM.length()),"@");
+ String propertyName = parts[1];
+ if ( AclModification.isDeny(k)) {
+ int td = toInt(e.getValue());
+ denies.put(propertyName, toInt(denies.get(propertyName)) | td);
+ } else if ( AclModification.isGrant(k)) {
+ int tg = toInt(e.getValue());
+ grants.put(propertyName, toInt(grants.get(propertyName)) | tg);
+ }
+ }
+ }
+ }
+ // if the property has been granted, then that should remove the deny
+ for ( Entry g : grants.entrySet()) {
+ String k = g.getKey();
+ if ( denies.containsKey(k)) {
+ denies.put(k, toInt(denies.get(k)) & ~g.getValue());
+ }
+ }
+ return new PropertyAcl(denies);
+ } finally {
+ compilingPermissions.dec();
+ }
+
+ }
+
+
+ public Map getEffectiveAcl(String objectType, String objectPath)
+ throws StorageClientException, AccessDeniedException {
+ throw new UnsupportedOperationException("Nag someone to implement this");
+ }
+
+ // to sign a token we need setAcl permissions on the delegate path
+ /**
+ * Content Tokens activate ACEs for a user that holds the content token. The
+ * token is signed by the secret key associated with the target Object/acl
+ * and the token is token content item is then returned for the caller to
+ * save.
+ */
+ public void signContentToken(Content token, String securityZone, String objectPath) throws StorageClientException, AccessDeniedException {
+ checkOpen();
+ check(Security.ZONE_CONTENT, objectPath, Permissions.CAN_WRITE_ACL);
+ check(Security.ZONE_CONTENT, objectPath, Permissions.CAN_READ_ACL);
+ String key = this.getAclKey(securityZone, objectPath);
+ Map currentAcl = getCached(keySpace, aclColumnFamily, key);
+ String secretKey = (String) currentAcl.get(_SECRET_KEY);
+ principalTokenValidator.signToken(token, secretKey);
+ // the caller must save the target.
+ }
+
+ @SuppressWarnings(value="RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE", justification="Not correct, the line in question doesnt check for a null, so the check is not redundant")
+ public void setAcl(String objectType, String objectPath, AclModification[] aclModifications)
+ throws StorageClientException, AccessDeniedException {
+ checkOpen();
+ check(objectType, objectPath, Permissions.CAN_WRITE_ACL);
+ check(objectType, objectPath, Permissions.CAN_READ_ACL);
+ String key = this.getAclKey(objectType, objectPath);
+ Map currentAcl = getCached(keySpace, aclColumnFamily, key);
+ if ( currentAcl == null ) {
+ currentAcl = Maps.newHashMap();
+ }
+ // every ACL gets a secret key, which avoids doing it later with a special call
+ Map modifications = Maps.newLinkedHashMap();
+ if ( !currentAcl.containsKey(_SECRET_KEY)) {
+ byte[] secretKeySeed = new byte[20];
+ secureRandom.nextBytes(secretKeySeed);
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA1");
+ modifications.put(_SECRET_KEY, Base64.encodeBase64URLSafeString(md.digest(secretKeySeed)));
+ } catch (NoSuchAlgorithmException e) {
+ LOGGER.error(e.getMessage(),e);
+ }
+ }
+ if ( !currentAcl.containsKey(_KEY)) {
+ modifications.put(_KEY, key);
+ modifications.put(_OBJECT_TYPE, objectType); // this is here to make data migration possible in the future
+ modifications.put(_PATH, objectPath); // same
+ }
+ for (AclModification m : aclModifications) {
+ String name = m.getAceKey();
+ if ( READ_ONLY_PROPERTIES.contains(name)) {
+ continue;
+ }
+ if (m.isRemove()) {
+ modifications.put(name, null);
+ } else {
+
+ int originalbitmap = getBitMap(name, modifications, currentAcl);
+ int modifiedbitmap = m.modify(originalbitmap);
+ LOGGER.debug("Adding Modification {} {} ",name, modifiedbitmap);
+ modifications.put(name, modifiedbitmap);
+
+ // KERN-1515
+ // We need to modify the opposite key to apply the
+ // reverse of the change we just made. Otherwise,
+ // you can end up with ACLs with contradictions, like:
+ // anonymous@g=1, anonymous@d=1
+ if (containsKey(inverseKeyOf(name), modifications, currentAcl)) {
+ // XOR gives us a mask of only the bits that changed
+ int difference = originalbitmap ^ modifiedbitmap;
+ int otherbitmap = toInt(getBitMap(inverseKeyOf(name), modifications, currentAcl));
+
+ // Zero out the bits that have been modified
+ //
+ // KERN-1887: This was originally toggling the modified bits
+ // using: "otherbitmap ^ difference", but this would
+ // incorrectly grant permissions in some cases (see JIRA
+ // issue). To avoid inconsistencies between grant and deny
+ // lists, setting a bit in one list should unset the
+ // corresponding bit in the other.
+ int modifiedotherbitmap = otherbitmap & ~difference;
+
+ if (otherbitmap != modifiedotherbitmap) {
+ // We made a change. Record our modification.
+ modifications.put(inverseKeyOf(name), modifiedotherbitmap);
+ }
+ }
+ }
+ }
+ LOGGER.debug("Updating ACL {} {} ", key, modifications);
+ putCached(keySpace, aclColumnFamily, key, modifications, (currentAcl == null || currentAcl.size() == 0));
+ storeListener.onUpdate(objectType, objectPath, getCurrentUserId(), "type:acl", false, null, "op:acl");
+ // clear the compiled cache for this session.
+ List keys = Lists.newArrayList();
+ for ( Entry e : cache.entrySet()) {
+ if (e.getKey().startsWith(key)) {
+ keys.add(e.getKey());
+ }
+ }
+ for ( String k : keys ) {
+ cache.remove(k);
+ }
+ }
+
+ private boolean containsKey(String name, Map map1,
+ Map map2) {
+ return map1.containsKey(name) || map2.containsKey(name);
+ }
+
+ private int getBitMap(String name, Map modifications,
+ Map currentAcl) {
+ int bm = 0;
+ if ( modifications.containsKey(name)) {
+ bm = toInt(modifications.get(name));
+ } else {
+ bm = toInt(currentAcl.get(name));
+ }
+ return bm;
+ }
+
+ private String inverseKeyOf(String key) {
+ if (key == null) {
+ return null;
+ }
+ if (AclModification.isGrant(key)) {
+ return AclModification.getPrincipal(key) + AclModification.DENIED_MARKER;
+ } else if (AclModification.isDeny(key)) {
+ return AclModification.getPrincipal(key) + AclModification.GRANTED_MARKER;
+ } else {
+ return key;
+ }
+ }
+
+ public void check(String objectType, String objectPath, Permission permission)
+ throws AccessDeniedException, StorageClientException {
+ if (user.isAdmin()) {
+ return;
+ }
+ if ( compilingPermissions.isSet() ) {
+ return;
+ }
+ // users can always operate on their own user object.
+ if (Security.ZONE_AUTHORIZABLES.equals(objectType) && user.getId().equals(objectPath)) {
+ return;
+ }
+ int[] privileges = compilePermission(user, objectType, objectPath, 0);
+ if (!((permission.getPermission() & privileges[0]) == permission.getPermission())) {
+ throw new AccessDeniedException(objectType, objectPath, permission.getName(),
+ user.getId());
+ }
+ }
+
+
+ private String getAclKey(String objectType, String objectPath) {
+ return objectType + ";" + objectPath;
+ }
+
+ public void setRequestPrincipalResolver(PrincipalTokenResolver principalTokenResolver ) {
+ this.principalTokenResolver = principalTokenResolver;
+ }
+ public void clearRequestPrincipalResolver() {
+ principalTokenResolver = null;
+ }
+
+ private int[] compilePermission(Authorizable authorizable, String objectType,
+ String objectPath, int recursion) throws StorageClientException {
+ String key = getAclKey(objectType, objectPath);
+ if (user.getId().equals(authorizable.getId()) && cache.containsKey(key)) {
+ return cache.get(key);
+ } else {
+ LOGGER.debug("Cache Miss {} [{}] ", cache, key);
+ }
+ try {
+ // we need to allow the permissions compile to bypass access control as it needs to see everything.
+ compilingPermissions.inc();
+ Map acl = getCached(keySpace, aclColumnFamily, key);
+ LOGGER.debug("ACL on {} is {} ", key, acl);
+
+ int grants = 0;
+ int denies = 0;
+ if (acl != null) {
+
+ {
+ String principal = authorizable.getId();
+ if ( principal.startsWith("_") ) {
+ throw new StorageClientException("Princials may not start with _ ");
+ }
+ int tg = toInt(acl.get(principal
+ + AclModification.GRANTED_MARKER));
+ int td = toInt(acl
+ .get(principal + AclModification.DENIED_MARKER));
+ grants = grants | tg;
+ denies = denies | td;
+ LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",new
+ Object[]{principal,tg,td,grants,denies});
+
+ }
+ /*
+ * Deal with any proxy principals, these override groups
+ */
+ if (principalTokenResolver != null) {
+ Set inspected = Sets.newHashSet();
+ if ( acl.containsKey(_SECRET_KEY)) {
+ String secretKey = (String) acl.get(_SECRET_KEY);
+ if ( secretKey != null ) {
+ for (Entry ace : acl.entrySet()) {
+ String k = ace.getKey();
+ LOGGER.debug("Checking {} ",k);
+ if (k.startsWith(DYNAMIC_PRINCIPAL_STEM)) {
+ String proxyPrincipal = AclModification.getPrincipal(k).substring(DYNAMIC_PRINCIPAL_STEM.length());
+ if ( !inspected.contains(proxyPrincipal)) {
+ inspected.add(proxyPrincipal);
+ LOGGER.debug("Is Dynamic {}, checking ",k);
+ try {
+ // principalTokenValidators are not safe code, hence we must re-enable full access control.
+ compilingPermissions.suspend();
+ List proxyPrincipalTokens = principalTokenResolver.resolveTokens(proxyPrincipal);
+ for ( Content proxyPrincipalToken : proxyPrincipalTokens ) {
+ if ( principalTokenValidator.validatePrincipal(proxyPrincipalToken, secretKey)) {
+ String pname = DYNAMIC_PRINCIPAL_STEM+proxyPrincipal;
+ LOGGER.debug("Has this principal {} ", proxyPrincipal);
+ int tg = toInt(acl.get(pname
+ + AclModification.GRANTED_MARKER));
+ int td = toInt(acl.get(pname
+ + AclModification.DENIED_MARKER));
+ grants = grants | tg;
+ denies = denies | td;
+ LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",new
+ Object[]{pname, tg,td,grants,denies});
+ break;
+ }
+ }
+ } finally {
+ // when done, we must resume compiling permissions where we were.
+ // NB, the code is re-entrant.
+ compilingPermissions.resume();
+ }
+ }
+ }
+ }
+ } else {
+ LOGGER.debug("Secret Key is null");
+ }
+ } else {
+ LOGGER.debug("No Secret Key Key ");
+ }
+ } else {
+ LOGGER.debug("No principalToken Resolver");
+ }
+ // then deal with static principals
+ for (String principal : getPrincipals(authorizable) ) {
+ if ( principal.startsWith("_") ) {
+ throw new StorageClientException("Princials may not start with _ ");
+ }
+ int tg = toInt(acl.get(principal
+ + AclModification.GRANTED_MARKER));
+ int td = toInt(acl
+ .get(principal + AclModification.DENIED_MARKER));
+ grants = grants | tg;
+ denies = denies | td;
+ LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",new
+ Object[]{principal,tg,td,grants,denies});
+ }
+
+ // Everyone must be the last principal to be applied
+ if (!User.ANON_USER.equals(authorizable.getId())) {
+ // all users except anon are in the group everyone, by default
+ // but only if not already denied or granted by a more specific
+ // permission.
+ int tg = (toInt(acl.get(Group.EVERYONE
+ + AclModification.GRANTED_MARKER)) & ~denies);
+ int td = (toInt(acl.get(Group.EVERYONE
+ + AclModification.DENIED_MARKER)) & ~grants);
+ grants = grants | tg;
+ denies = denies | td;
+ LOGGER.debug("Added Permissions for {} g{} d{} => g{} d{}",new
+ Object[]{Group.EVERYONE,tg,td,grants,denies});
+
+ }
+ /*
+ * grants contains the granted permissions in a bitmap denies
+ * contains the denied permissions in a bitmap
+ */
+ int granted = grants;
+ int denied = denies;
+
+ /*
+ * Only look to parent objects if this is not the root object and
+ * everything is not granted and denied
+ */
+ if (recursion < 20 && !StorageClientUtils.isRoot(objectPath)
+ && (granted != 0xffff || denied != 0xffff)) {
+ recursion++;
+ int[] parentPriv = compilePermission(authorizable, objectType,
+ StorageClientUtils.getParentObjectPath(objectPath), recursion);
+ if (parentPriv != null) {
+ /*
+ * Grant permission not denied at this level parentPriv[0]
+ * is permissions granted by the parent ~denies is
+ * permissions not denied here parentPriv[0] & ~denies is
+ * permissions granted by the parent that have not been
+ * denied here. we need to add those to things granted here.
+ * ie |
+ */
+ granted = grants | (parentPriv[0] & ~denies);
+ /*
+ * Deny permissions not granted at this level
+ */
+ denied = denies | (parentPriv[1] & ~grants);
+ }
+ }
+ // If not denied all users and groups can read other users and
+ // groups and all content can be read
+ if (((denied & Permissions.CAN_READ.getPermission()) == 0)
+ && (Security.ZONE_AUTHORIZABLES.equals(objectType) || Security.ZONE_CONTENT
+ .equals(objectType))) {
+ granted = granted | Permissions.CAN_READ.getPermission();
+ LOGGER.debug("Default Read Permission set {} {} ",key,denied);
+ } else {
+ LOGGER.debug("Default Read has been denied {} {} ",key,
+ denied);
+ }
+ LOGGER.debug("Permissions on {} for {} is {} {} ",new
+ Object[]{key,user.getId(),granted,denied});
+ /*
+ * Keep a cached copy
+ */
+ if (user.getId().equals(authorizable.getId())) {
+ cache.put(key, new int[] { granted, denied });
+ }
+ return new int[] { granted, denied };
+
+ }
+ if (Security.ZONE_AUTHORIZABLES.equals(objectType)
+ || Security.ZONE_CONTENT.equals(objectType)) {
+ // unless explicitly denied all users can read other users.
+ return new int[] { Permissions.CAN_READ.getPermission(), 0 };
+ }
+ return new int[] { 0, 0 };
+ } finally {
+ // decrement the counter from here.
+ compilingPermissions.dec();
+ }
+ }
+
+
+ private String[] getPrincipals(final Authorizable authorizable) {
+ String k = authorizable.getId();
+ if (principalCache.containsKey(k)) {
+ return principalCache.get(k);
+ }
+ Set memberOfSet = Sets.newHashSet(authorizable.getPrincipals());
+ if ( authorizableManager != null ) {
+ // membership resolution is possible, but we had better turn off recursion
+ if ( principalRecursionLock.get() == null ) {
+ principalRecursionLock.set("l");
+ try {
+ for ( Iterator gi = authorizable.memberOf(authorizableManager); gi.hasNext(); ) {
+ memberOfSet.add(gi.next().getId());
+ }
+ } finally {
+ principalRecursionLock.set(null);
+ }
+ }
+ }
+ memberOfSet.remove(Group.EVERYONE);
+ String[] m = memberOfSet.toArray(new String[memberOfSet.size()]);
+ principalCache.put(k, m);
+ return m;
+ }
+
+
+ private int toInt(Object object) {
+ if ( object instanceof Integer ) {
+ return ((Integer) object).intValue();
+ }
+ LOGGER.debug("Bitmap Not Present");
+ return 0;
+ }
+
+ public String getCurrentUserId() {
+ return user.getId();
+ }
+
+ public void close() {
+ closed = true;
+ }
+
+ private void checkOpen() throws StorageClientException {
+ if (closed) {
+ throw new StorageClientException("Access Control Manager is closed");
+ }
+ }
+
+ public boolean can(Authorizable authorizable, String objectType, String objectPath,
+ Permission permission) {
+ if ( compilingPermissions.isSet() ) {
+ return true;
+ }
+ if (authorizable instanceof User && ((User) authorizable).isAdmin()) {
+ return true;
+ }
+ // users can always operate on their own user object.
+ if (Security.ZONE_AUTHORIZABLES.equals(objectType)
+ && authorizable.getId().equals(objectPath)) {
+ return true;
+ }
+ try {
+ int[] privileges = compilePermission(authorizable, objectType, objectPath, 0);
+ if (!((permission.getPermission() & privileges[0]) == permission.getPermission())) {
+ return false;
+ }
+ } catch (StorageClientException e) {
+ LOGGER.warn(e.getMessage(), e);
+ return false;
+ }
+ return true;
+ }
+
+ public Permission[] getPermissions(String objectType, String path) throws StorageClientException {
+ int[] perms = compilePermission(this.user, objectType, path, 0);
+ List permissions = Lists.newArrayList();
+ for (Permission p : Permissions.PRIMARY_PERMISSIONS) {
+ if ((perms[0] & p.getPermission()) == p.getPermission()) {
+ permissions.add(p);
+ }
+ }
+ return permissions.toArray(new Permission[permissions.size()]);
+ }
+
+ public String[] findPrincipals(String objectType, String objectPath, int permission, boolean granted) throws StorageClientException {
+ Map principalMap = internalCompilePrincipals(objectType, objectPath, 0);
+ LOGGER.debug("Got Principals {} ",principalMap);
+ List principals = Lists.newArrayList();
+ for (Entry perm : principalMap.entrySet()) {
+ int[] p = perm.getValue();
+ if ( granted && (p[0] & permission) == permission ) {
+ principals.add(perm.getKey());
+ LOGGER.debug("Included {} {} {} ",new Object[]{perm.getKey(), perm.getValue(), permission});
+ } else if ( !granted && (p[1] & permission) == permission) {
+ principals.add(perm.getKey());
+ LOGGER.debug("Included {} {} {} ",new Object[]{perm.getKey(), perm.getValue(), permission});
+ } else {
+ LOGGER.debug("Filtered {} {} {} ",new Object[]{perm.getKey(), perm.getValue(), permission});
+ }
+ }
+ LOGGER.debug(" Found Principals {} ",principals);
+ return principals.toArray(new String[principals.size()]);
+ }
+
+
+
+ private Map internalCompilePrincipals(String objectType, String objectPath, int recursion) throws StorageClientException {
+ Map compiledPermissions = Maps.newHashMap();
+ String key = getAclKey(objectType, objectPath);
+
+ Map acl = getCached(keySpace, aclColumnFamily, key);
+
+ if (acl != null) {
+ LOGGER.debug("Checking {} {} ",key,acl);
+ for (Entry ace : acl.entrySet()) {
+ String aceKey = ace.getKey();
+ String principal = aceKey.substring(0, aceKey.length() - 2);
+
+ if (!compiledPermissions.containsKey(principal)) {
+ int tg = toInt(acl.get(principal
+ + AclModification.GRANTED_MARKER));
+ int td = toInt(acl.get(principal
+ + AclModification.DENIED_MARKER));
+ compiledPermissions.put(principal, new int[] { tg, td });
+ LOGGER.debug("added {} ",principal);
+ }
+
+ }
+ }
+ /*
+ * grants contains the granted permissions in a bitmap denies contains
+ * the denied permissions in a bitmap
+ */
+
+ /*
+ * Only look to parent objects if this is not the root object and
+ * everything is not granted and denied
+ */
+ if (recursion < 20 && !StorageClientUtils.isRoot(objectPath)) {
+ recursion++;
+ Map parentPermissions = internalCompilePrincipals(objectType,
+ StorageClientUtils.getParentObjectPath(objectPath), recursion);
+ // add the parernt privileges in
+ for (Entry parentPermission : parentPermissions.entrySet()) {
+ int[] thisPriv = new int[2];
+ String principal = parentPermission.getKey();
+ if (compiledPermissions.containsKey(principal)) {
+ thisPriv = compiledPermissions.get(principal);
+ LOGGER.debug("modified {} ",principal);
+ } else {
+ LOGGER.debug("creating {} ",principal);
+ }
+ int[] parentPriv = parentPermission.getValue();
+
+ /*
+ * Grant permission not denied at this level parentPriv[0] is
+ * permissions granted by the parent ~denies is permissions not
+ * denied here parentPriv[0] & ~denies is permissions granted by
+ * the parent that have not been denied here. we need to add
+ * those to things granted here. ie |
+ */
+ int granted = thisPriv[0] | (parentPriv[0] & ~thisPriv[1]);
+ /*
+ * Deny permissions not granted at this level
+ */
+ int denied = thisPriv[1] | (parentPriv[1] & ~thisPriv[0]);
+
+ compiledPermissions.put(principal, new int[] { granted, denied });
+
+ }
+ }
+
+ //
+ // If not denied all users and groups can read other users and
+ // groups and all content can be read
+ for (String principal : new String[] { Group.EVERYONE, User.ANON_USER }) {
+ int[] perm = new int[2];
+ if (compiledPermissions.containsKey(principal)) {
+ perm = compiledPermissions.get(principal);
+ }
+ if (((perm[1] & Permissions.CAN_READ.getPermission()) == 0)
+ && (Security.ZONE_AUTHORIZABLES.equals(objectType) || Security.ZONE_CONTENT
+ .equals(objectType))) {
+ perm[0] = perm[0] | Permissions.CAN_READ.getPermission();
+ LOGGER.debug("added Default {} ",principal);
+ compiledPermissions.put(principal, perm);
+ }
+ }
+ compiledPermissions.put(User.ADMIN_USER, new int[] { 0xffff, 0x0000});
+ return compiledPermissions;
+ // only store those permissions the match the requested set.]
+
+
+ }
+
+ @Override
+ protected Logger getLogger() {
+ return LOGGER;
+ }
+
+ public void setAuthorizableManager(AuthorizableManager authorizableManager) {
+ this.authorizableManager = authorizableManager;
+ }
+
+
+
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlledMap.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlledMap.java
new file mode 100644
index 00000000..5681d6e3
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AccessControlledMap.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+
+public class AccessControlledMap extends HashMap {
+
+
+ private PropertyAcl propertyAcl;
+
+ public AccessControlledMap(PropertyAcl propertyAcl) {
+ this.propertyAcl = propertyAcl;
+ }
+ /**
+ *
+ */
+ private static final long serialVersionUID = -6550830558631198709L;
+
+ @Override
+ public V put(K key, V value) {
+ if ( propertyAcl.canWrite(key)) {
+ return super.put(key, value);
+ }
+ return null;
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ for ( Entry extends K, ? extends V> e : m.entrySet()) {
+ put(e.getKey(), e.getValue());
+ }
+ }
+
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java
similarity index 61%
rename from src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java
index 96260f71..5d80c524 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/AuthenticatorImpl.java
@@ -17,35 +17,37 @@
*/
package org.sakaiproject.nakamura.lite.accesscontrol;
+import java.util.Map;
+
+import org.sakaiproject.nakamura.api.lite.CacheHolder;
import org.sakaiproject.nakamura.api.lite.Configuration;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.accesscontrol.Authenticator;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
+import org.sakaiproject.nakamura.api.lite.util.EnabledPeriod;
+import org.sakaiproject.nakamura.lite.CachingManagerImpl;
import org.sakaiproject.nakamura.lite.authorizable.UserInternal;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Map;
-
-public class AuthenticatorImpl implements Authenticator {
+public class AuthenticatorImpl extends CachingManagerImpl implements Authenticator {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticatorImpl.class);
- private StorageClient client;
private String keySpace;
private String authorizableColumnFamily;
- public AuthenticatorImpl(StorageClient client, Configuration configuration) {
- this.client = client;
+ public AuthenticatorImpl(StorageClient client, Configuration configuration, Map sharedCache) {
+ super(client, sharedCache);
this.keySpace = configuration.getKeySpace();
this.authorizableColumnFamily = configuration.getAuthorizableColumnFamily();
}
public User authenticate(String userid, String password) {
try {
- Map userAuthMap = client
- .get(keySpace, authorizableColumnFamily, userid);
+ Map userAuthMap = getCached(keySpace, authorizableColumnFamily, userid);
if (userAuthMap == null) {
LOGGER.debug("User was not found {}", userid);
return null;
@@ -55,29 +57,48 @@ public User authenticate(String userid, String password) {
String storedPassword = (String) userAuthMap
.get(User.PASSWORD_FIELD);
if (passwordHash.equals(storedPassword)) {
- return new UserInternal(userAuthMap, false);
+ if ( EnabledPeriod.isInEnabledPeriod((String) userAuthMap.get(User.LOGIN_ENABLED_PERIOD_FIELD)) ) {
+ return new UserInternal(userAuthMap, null, false);
+ }
}
LOGGER.debug("Failed to authentication, passwords did not match");
} catch (StorageClientException e) {
LOGGER.debug("Failed To authenticate " + e.getMessage(), e);
+ } catch (AccessDeniedException e) {
+ LOGGER.debug("Failed To system authenticate user " + e.getMessage(), e);
}
return null;
}
-
public User systemAuthenticate(String userid) {
+ return internalSystemAuthenticate(userid, false);
+ }
+ public User systemAuthenticateBypassEnable(String userid) {
+ return internalSystemAuthenticate(userid, true);
+ }
+
+ private User internalSystemAuthenticate(String userid, boolean forceEnableLogin) {
try {
- Map userAuthMap = client
- .get(keySpace, authorizableColumnFamily, userid);
+ Map userAuthMap = getCached(keySpace, authorizableColumnFamily, userid);
if (userAuthMap == null || userAuthMap.size() == 0) {
LOGGER.debug("User was not found {}", userid);
return null;
}
- return new UserInternal(userAuthMap, false);
+ if ( forceEnableLogin || EnabledPeriod.isInEnabledPeriod((String) userAuthMap.get(User.LOGIN_ENABLED_PERIOD_FIELD)) ) {
+ return new UserInternal(userAuthMap, null, false);
+ }
} catch (StorageClientException e) {
LOGGER.debug("Failed To system authenticate user " + e.getMessage(), e);
+ } catch (AccessDeniedException e) {
+ LOGGER.debug("Failed To system authenticate user " + e.getMessage(), e);
}
return null;
}
+ @Override
+ protected Logger getLogger() {
+ return LOGGER;
+ }
+
+
}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/DefaultPrincipalValidator.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/DefaultPrincipalValidator.java
new file mode 100644
index 00000000..8c7a5e44
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/DefaultPrincipalValidator.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorPlugin;
+import org.sakaiproject.nakamura.api.lite.content.Content;
+
+public class DefaultPrincipalValidator implements PrincipalValidatorPlugin {
+
+ public boolean validate(Content proxyPrincipalToken) {
+ // TODO add some standard validation steps like date
+ return true;
+ }
+
+ public String[] getProtectedFields() {
+ return new String[0];
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalTokenValidator.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalTokenValidator.java
new file mode 100644
index 00000000..7faa6cae
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalTokenValidator.java
@@ -0,0 +1,144 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import org.apache.commons.codec.binary.Base64;
+import org.sakaiproject.nakamura.api.lite.StorageClientException;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorPlugin;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
+import org.sakaiproject.nakamura.api.lite.content.Content;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class PrincipalTokenValidator {
+
+
+ public static final String VALIDATORPLUGIN = "validatorplugin";
+ public static final String _ACLTOKEN = "_acltoken";
+ private static final String HMAC_SHA512 = "HmacSHA512";
+ private static final Logger LOGGER = LoggerFactory.getLogger(PrincipalTokenValidator.class);
+ private PrincipalValidatorPlugin defaultPrincipalValidator = new DefaultPrincipalValidator();
+ private PrincipalValidatorResolver principalValidatorResolver;
+
+ public PrincipalTokenValidator(PrincipalValidatorResolver principalValidatorResolver) {
+ this.principalValidatorResolver = principalValidatorResolver;
+ }
+
+ public boolean validatePrincipal(Content proxyPrincipalToken, String sharedKey) {
+ if ( proxyPrincipalToken == null) {
+ LOGGER.debug("Failed to Validate Token at no content item ");
+ return false;
+ }
+ if ( !proxyPrincipalToken.hasProperty(_ACLTOKEN)) {
+ LOGGER.debug("Failed to Validate Token at {} no ACL Token ", proxyPrincipalToken.getPath());
+ return false;
+ }
+ PrincipalValidatorPlugin plugin = null;
+ if ( proxyPrincipalToken.hasProperty(VALIDATORPLUGIN) ) {
+ plugin = principalValidatorResolver.getPluginByName((String) proxyPrincipalToken.getProperty(VALIDATORPLUGIN));
+ } else {
+ plugin = defaultPrincipalValidator;
+ }
+ if ( plugin == null ) {
+ LOGGER.debug("Failed to Validate Token at {} no plugin ");
+ return false;
+ }
+ String hmac = signToken(proxyPrincipalToken, sharedKey, plugin);
+ if ( hmac == null || !hmac.equals(proxyPrincipalToken.getProperty(_ACLTOKEN)) ) {
+ LOGGER.debug("Failed to Validate Token at {} as {}, does not match ",proxyPrincipalToken.getPath(), hmac);
+ return false;
+ }
+ boolean validate = plugin.validate(proxyPrincipalToken);
+ if ( validate ) {
+ LOGGER.debug("Validated Token at {} as {} using plugin {} ",new Object[] { proxyPrincipalToken.getPath(), hmac, plugin});
+ } else {
+ LOGGER.debug("Invalid Token at {} as {} using plugin {} ",new Object[] { proxyPrincipalToken.getPath(), hmac, plugin});
+ }
+ return validate;
+ }
+
+ public void signToken(Content token, String sharedKey ) throws StorageClientException {
+ PrincipalValidatorPlugin plugin = null;
+ if ( token.hasProperty(VALIDATORPLUGIN) ) {
+ plugin = principalValidatorResolver.getPluginByName((String) token.getProperty(VALIDATORPLUGIN));
+ } else {
+ plugin = defaultPrincipalValidator;
+ }
+ if ( plugin == null ) {
+ throw new StorageClientException("The property validatorplugin does not specify an active PricipalValidatorPlugin, cant sign");
+ }
+ token.setProperty(_ACLTOKEN, signToken(token, sharedKey, plugin));
+ }
+
+ private String signToken(Content token, String sharedKey, PrincipalValidatorPlugin plugin) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-512");
+ byte[] input = sharedKey.getBytes("UTF-8");
+ byte[] data = md.digest(input);
+ SecretKeySpec key = new SecretKeySpec(data, HMAC_SHA512);
+ return getHmac(token, plugin.getProtectedFields(), key);
+ } catch (InvalidKeyException e) {
+ LOGGER.warn(e.getMessage());
+ LOGGER.debug(e.getMessage(),e);
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ LOGGER.warn(e.getMessage());
+ LOGGER.debug(e.getMessage(),e);
+ return null;
+ } catch (IllegalStateException e) {
+ LOGGER.warn(e.getMessage());
+ LOGGER.debug(e.getMessage(),e);
+ return null;
+ } catch (UnsupportedEncodingException e) {
+ LOGGER.warn(e.getMessage());
+ LOGGER.debug(e.getMessage(),e);
+ return null;
+ }
+ }
+
+
+ private String getHmac(Content principalToken, String[] extraFields, SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException, IllegalStateException, UnsupportedEncodingException {
+ StringBuilder sb = new StringBuilder();
+ sb.append(principalToken.getPath()).append("@");
+ if ( principalToken.hasProperty(VALIDATORPLUGIN)) {
+ sb.append(principalToken.getProperty(VALIDATORPLUGIN)).append("@");
+ }
+ for (String f : extraFields) {
+ if ( principalToken.hasProperty(f)) {
+ sb.append(principalToken.getProperty(f)).append("@");
+ } else {
+ sb.append("null").append("@");
+ }
+ }
+ Mac m = Mac.getInstance(HMAC_SHA512);
+ m.init(key);
+ String message = sb.toString();
+ LOGGER.debug("Signing {} ", message);
+ m.update(message.getBytes("UTF-8"));
+ return Base64.encodeBase64URLSafeString(m.doFinal());
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalValidatorResolverImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalValidatorResolverImpl.java
new file mode 100644
index 00000000..b4ad2444
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PrincipalValidatorResolverImpl.java
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import com.google.common.collect.Maps;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Service;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorPlugin;
+import org.sakaiproject.nakamura.api.lite.accesscontrol.PrincipalValidatorResolver;
+
+import java.util.Map;
+
+@Component(immediate=true, metatype=true, enabled=true)
+@Service(value=PrincipalValidatorResolver.class)
+public class PrincipalValidatorResolverImpl implements PrincipalValidatorResolver {
+
+ protected Map pluginStore = Maps.newConcurrentMap();
+
+ public PrincipalValidatorPlugin getPluginByName(String key) {
+ return pluginStore.get(key);
+ }
+
+ public void registerPlugin(String key, PrincipalValidatorPlugin plugin) {
+ pluginStore.put(key, plugin);
+ }
+ public void unregisterPlugin(String key) {
+ pluginStore.remove(key);
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PropertyAcl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PropertyAcl.java
new file mode 100644
index 00000000..deab4bb4
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/PropertyAcl.java
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.sakaiproject.nakamura.api.lite.accesscontrol.Permissions;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+public class PropertyAcl implements Serializable {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = -3998584870894631478L;
+ private Set readDenied;
+ private Set writeDenied;
+
+ public PropertyAcl(Map denies) {
+ Set r = Sets.newHashSet();
+ Set w = Sets.newHashSet();
+ for (Entry ace : denies.entrySet()) {
+ if ((Permissions.CAN_READ_PROPERTY.getPermission() & ace.getValue()) == Permissions.CAN_READ_PROPERTY
+ .getPermission()) {
+ r.add(ace.getKey());
+ }
+ if ((Permissions.CAN_WRITE_PROPERTY.getPermission() & ace.getValue()) == Permissions.CAN_WRITE_PROPERTY
+ .getPermission()) {
+ w.add(ace.getKey());
+ }
+ }
+ readDenied = ImmutableSet.copyOf(r.toArray(new String[r.size()]));
+ writeDenied = ImmutableSet.copyOf(w.toArray(new String[w.size()]));
+ }
+
+ public PropertyAcl() {
+ readDenied = ImmutableSet.of();
+ writeDenied = ImmutableSet.of();
+ }
+
+ public Set readDeniedSet() {
+ return readDenied;
+ }
+
+ public boolean canWrite(Object key) {
+ return !writeDenied.contains(key);
+ }
+
+}
diff --git a/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/ThreadBoundStackReferenceCounter.java b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/ThreadBoundStackReferenceCounter.java
new file mode 100644
index 00000000..428a2701
--- /dev/null
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/accesscontrol/ThreadBoundStackReferenceCounter.java
@@ -0,0 +1,99 @@
+/**
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.sakaiproject.nakamura.lite.accesscontrol;
+
+import java.util.List;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Maintains a thread bound reference counter that can be suspended and resumed.
+ * When suspended the current counter us pushed to a stack, and the new counter
+ * is started. When resumed, the current counter is replaced with the counter on
+ * the stack. The operations are all bound to the current thread. inc() dec()
+ * and suspend() resume() should be used in matching pairs protected by try {
+ * ... } finally { ... } constructs. The class makes no attempt to guess what
+ * the code is doing.
+ *
+ * @author ieb
+ *
+ */
+public class ThreadBoundStackReferenceCounter {
+
+ // dont use initial value to avoid JVM bugs.
+ private ThreadLocal counter = new ThreadLocal();
+ private ThreadLocal> suspended = new ThreadLocal>();
+
+ public void inc() {
+ set(get() + 1);
+ }
+
+ public void dec() {
+ set(get() - 1);
+ }
+
+ public void suspend() {
+ push(get());
+ set(0);
+ }
+
+ public void resume() {
+ set(pop());
+ }
+
+ public boolean isSet() {
+ return get() > 0;
+ }
+
+ private int get() {
+ Integer c = counter.get();
+ if (c == null) {
+ return 0;
+ }
+ return c.intValue();
+ }
+
+ private void set(int i) {
+ if (i < 0) {
+ i = 0;
+ }
+ counter.set(i);
+ }
+
+ private void push(int i) {
+ List s = suspended.get();
+ if (s == null) {
+ s = Lists.newArrayList();
+ suspended.set(s);
+ }
+ s.add(i);
+ }
+
+ private int pop() {
+ List s = suspended.get();
+ if (s == null) {
+ s = Lists.newArrayList();
+ suspended.set(s);
+ }
+ if (s.size() == 0) {
+ return 0;
+ }
+ return s.remove(s.size() - 1);
+ }
+
+}
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java b/core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java
similarity index 95%
rename from src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java
index e5fe9640..b982287f 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableActivator.java
@@ -25,7 +25,7 @@
import org.sakaiproject.nakamura.api.lite.accesscontrol.AccessDeniedException;
import org.sakaiproject.nakamura.api.lite.authorizable.Authorizable;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -77,7 +77,7 @@ private void createSystemUser() throws StorageClientException {
User.SYSTEM_USER);
if (authorizableMap == null || authorizableMap.size() == 0) {
Map user = ImmutableMap.of(Authorizable.ID_FIELD,
- User.SYSTEM_USER, Authorizable.NAME_FIELD,
+ (Object)User.SYSTEM_USER, Authorizable.NAME_FIELD,
User.SYSTEM_USER, Authorizable.PASSWORD_FIELD,
"--no-password--",
Authorizable.AUTHORIZABLE_TYPE_FIELD, Authorizable.USER_VALUE);
@@ -94,7 +94,7 @@ private void createAdminUser() throws StorageClientException {
User.ADMIN_USER);
if (authorizableMap == null || authorizableMap.size() == 0) {
Map user = ImmutableMap.of(Authorizable.ID_FIELD,
- User.ADMIN_USER, Authorizable.NAME_FIELD,
+ (Object)User.ADMIN_USER, Authorizable.NAME_FIELD,
User.ADMIN_USER, Authorizable.PASSWORD_FIELD,
StorageClientUtils.secureHash("admin"),
Authorizable.AUTHORIZABLE_TYPE_FIELD, Authorizable.USER_VALUE);
@@ -110,7 +110,7 @@ private void createAnonUser() throws StorageClientException {
User.ANON_USER);
if (authorizableMap == null || authorizableMap.size() == 0) {
Map user = ImmutableMap.of(Authorizable.ID_FIELD,
- User.ANON_USER, Authorizable.NAME_FIELD,
+ (Object)User.ANON_USER, Authorizable.NAME_FIELD,
User.ANON_USER, Authorizable.PASSWORD_FIELD,
Authorizable.NO_PASSWORD,
Authorizable.AUTHORIZABLE_TYPE_FIELD, Authorizable.USER_VALUE);
diff --git a/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java b/core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java
similarity index 63%
rename from src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java
rename to core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java
index 4fedb9b7..7ffc0e42 100644
--- a/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java
+++ b/core/src/main/java/org/sakaiproject/nakamura/lite/authorizable/AuthorizableManagerImpl.java
@@ -17,13 +17,14 @@
*/
package org.sakaiproject.nakamura.lite.authorizable;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableMap.Builder;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Maps;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.commons.lang.StringUtils;
import org.sakaiproject.nakamura.api.lite.CacheHolder;
import org.sakaiproject.nakamura.api.lite.Configuration;
+import org.sakaiproject.nakamura.api.lite.Session;
import org.sakaiproject.nakamura.api.lite.StorageClientException;
import org.sakaiproject.nakamura.api.lite.StorageClientUtils;
import org.sakaiproject.nakamura.api.lite.StoreListener;
@@ -37,16 +38,20 @@
import org.sakaiproject.nakamura.api.lite.authorizable.Group;
import org.sakaiproject.nakamura.api.lite.authorizable.User;
import org.sakaiproject.nakamura.api.lite.util.PreemptiveIterator;
-import org.sakaiproject.nakamura.lite.CachingManager;
+import org.sakaiproject.nakamura.lite.CachingManagerImpl;
+import org.sakaiproject.nakamura.lite.accesscontrol.AccessControlManagerImpl;
import org.sakaiproject.nakamura.lite.accesscontrol.AuthenticatorImpl;
-import org.sakaiproject.nakamura.lite.storage.StorageClient;
+import org.sakaiproject.nakamura.lite.storage.spi.DisposableIterator;
+import org.sakaiproject.nakamura.lite.storage.spi.SparseRow;
+import org.sakaiproject.nakamura.lite.storage.spi.StorageClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
/**
* An Authourizable Manager bound to a user, on creation the user ID specified
@@ -55,11 +60,16 @@
* @author ieb
*
*/
-public class AuthorizableManagerImpl extends CachingManager implements AuthorizableManager {
+public class AuthorizableManagerImpl extends CachingManagerImpl implements AuthorizableManager {
+ private static final String DISABLED_PASSWORD_HASH = "--disabled--";
private static final Set FILTER_ON_UPDATE = ImmutableSet.of(Authorizable.ID_FIELD,
- Authorizable.PASSWORD_FIELD);
+ Authorizable.PASSWORD_FIELD, Authorizable.LOGIN_ENABLED_PERIOD_FIELD);
private static final Set FILTER_ON_CREATE = ImmutableSet.of(Authorizable.ID_FIELD,
+ Authorizable.PASSWORD_FIELD, Authorizable.LOGIN_ENABLED_PERIOD_FIELD);
+ private static final Set ADMIN_FILTER_ON_UPDATE = ImmutableSet.of(Authorizable.ID_FIELD,
+ Authorizable.PASSWORD_FIELD);
+ private static final Set ADMIN_FILTER_ON_CREATE = ImmutableSet.of(Authorizable.ID_FIELD,
Authorizable.PASSWORD_FIELD);
private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizableManagerImpl.class);
private String currentUserId;
@@ -71,9 +81,12 @@ public class AuthorizableManagerImpl extends CachingManager implements Authoriza
private boolean closed;
private Authenticator authenticator;
private StoreListener storeListener;
+ private Session session;
+ private Set filterOnUpdate;
+ private Set filterOnCreate;
- public AuthorizableManagerImpl(User currentUser, StorageClient client,
- Configuration configuration, AccessControlManager accessControlManager,
+ public AuthorizableManagerImpl(User currentUser, Session session, StorageClient client,
+ Configuration configuration, AccessControlManagerImpl accessControlManager,
Map sharedCache, StoreListener storeListener) throws StorageClientException,
AccessDeniedException {
super(client, sharedCache);
@@ -82,13 +95,22 @@ public AuthorizableManagerImpl(User currentUser, StorageClient client,
throw new RuntimeException("Current User ID shoud not be null");
}
this.thisUser = currentUser;
+ if ( thisUser.isAdmin() ) {
+ filterOnUpdate = ADMIN_FILTER_ON_UPDATE;
+ filterOnCreate = ADMIN_FILTER_ON_CREATE;
+ } else {
+ filterOnUpdate = FILTER_ON_UPDATE;
+ filterOnCreate = FILTER_ON_CREATE;
+ }
+ this.session = session;
this.client = client;
this.accessControlManager = accessControlManager;
this.keySpace = configuration.getKeySpace();
this.authorizableColumnFamily = configuration.getAuthorizableColumnFamily();
- this.authenticator = new AuthenticatorImpl(client, configuration);
+ this.authenticator = new AuthenticatorImpl(client, configuration, sharedCache);
this.closed = false;
this.storeListener = storeListener;
+ accessControlManager.setAuthorizableManager(this);
}
public User getUser() {
@@ -112,18 +134,40 @@ public Authorizable findAuthorizable(final String authorizableId) throws AccessD
return null;
}
if (isAUser(authorizableMap)) {
- return new UserInternal(authorizableMap, false);
+ return new UserInternal(authorizableMap, session, false);
} else if (isAGroup(authorizableMap)) {
- return new GroupInternal(authorizableMap, false);
+ return new GroupInternal(authorizableMap, session, false);
}
return null;
}
public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedException,
StorageClientException {
+ updateAuthorizable(authorizable, true);
+ }
+
+ public void updateAuthorizable(Authorizable authorizable, boolean withTouch) throws AccessDeniedException,
+ StorageClientException {
checkOpen();
+ if ( !withTouch && !thisUser.isAdmin() ) {
+ throw new StorageClientException("Only admin users can update without touching the user");
+ }
String id = authorizable.getId();
+ if ( authorizable.isImmutable() ) {
+ throw new StorageClientException("You cant update an immutable authorizable:"+id);
+ }
+ if ( authorizable.isReadOnly() ) {
+ return;
+ }
+ if ( authorizable.isNew() ) {
+ throw new StorageClientException("You must create an authorizable if its new, you cant update an new authorizable");
+ }
accessControlManager.check(Security.ZONE_AUTHORIZABLES, id, Permissions.CAN_WRITE);
+ if ( !authorizable.isModified() ) {
+ return;
+ // only perform the update and send the event if we see the authorizable as modified. It will be modified ig group membership was changed.
+ }
+
/*
* Update the principal records for members. The list of members that
* have been added and removed is converted into a list of Authorzables.
@@ -138,10 +182,14 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
* permission at some point in the future.
*/
String type = "type:user";
+ List attributes = Lists.newArrayList();
+ String[] membersAdded = null;
+ String[] membersRemoved = null;
+
if (authorizable instanceof Group) {
type = "type:group";
Group group = (Group) authorizable;
- String[] membersAdded = group.getMembersAdded();
+ membersAdded = group.getMembersAdded();
Authorizable[] newMembers = new Authorizable[membersAdded.length];
int i = 0;
for (String newMember : membersAdded) {
@@ -166,7 +214,7 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
i++;
}
i = 0;
- String[] membersRemoved = group.getMembersRemoved();
+ membersRemoved = group.getMembersRemoved();
Authorizable[] retiredMembers = new Authorizable[membersRemoved.length];
for (String retiredMember : membersRemoved) {
try {
@@ -181,7 +229,9 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
}
- LOGGER.debug("Membership Change added [{}] removed [{}] ", Arrays.toString(newMembers), Arrays.toString(retiredMembers));
+ String membersAddedCsv = StringUtils.join(membersAdded, ',');
+ String membersRemovedCsv = StringUtils.join(membersRemoved, ',');
+ LOGGER.debug("Membership Change added [{}] removed [{}] ", membersAddedCsv, membersRemovedCsv);
int changes = 0;
// there is now a sparse list of authorizables, that need changing
for (Authorizable newMember : newMembers) {
@@ -190,7 +240,8 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
if (newMember.isModified()) {
Map encodedProperties = StorageClientUtils
.getFilteredAndEcodedMap(newMember.getPropertiesForUpdate(),
- FILTER_ON_UPDATE);
+ filterOnUpdate);
+ encodedProperties.put(Authorizable.ID_FIELD, newMember.getId());
putCached(keySpace, authorizableColumnFamily, newMember.getId(),
encodedProperties, newMember.isNew());
LOGGER.debug("Updated {} with principal {} {} ",new Object[]{newMember.getId(), group.getId(), encodedProperties});
@@ -208,7 +259,8 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
if (retiredMember.isModified()) {
Map encodedProperties = StorageClientUtils
.getFilteredAndEcodedMap(retiredMember.getPropertiesForUpdate(),
- FILTER_ON_UPDATE);
+ filterOnUpdate);
+ encodedProperties.put(Authorizable.ID_FIELD, retiredMember.getId());
putCached(keySpace, authorizableColumnFamily, retiredMember.getId(),
encodedProperties, retiredMember.isNew());
changes++;
@@ -220,24 +272,56 @@ public void updateAuthorizable(Authorizable authorizable) throws AccessDeniedExc
}
}
LOGGER.debug(" Finished Updating other principals, made {} changes, Saving Changes to {} ", changes, id);
- }
+ // if there were added or removed members, send them out as event properties for
+ // external integration
+ if (membersAdded.length > 0) {
+ attributes.add("added:" + membersAddedCsv);
+ }
+ if (membersRemoved.length > 0) {
+ attributes.add("removed:" + membersRemovedCsv);
+ }
+ }
+ attributes.add(type);
+ boolean wasNew = authorizable.isNew();
+ Map beforeUpdateProperties = authorizable.getOriginalProperties();
Map encodedProperties = StorageClientUtils.getFilteredAndEcodedMap(
- authorizable.getPropertiesForUpdate(), FILTER_ON_UPDATE);
- encodedProperties.put(Authorizable.LASTMODIFIED,System.currentTimeMillis());
- encodedProperties.put(Authorizable.LASTMODIFIED_BY,accessControlManager.getCurrentUserId());
+ authorizable.getPropertiesForUpdate(), filterOnUpdate);
+ if (withTouch) {
+ encodedProperties.put(Authorizable.LASTMODIFIED_FIELD, System.currentTimeMillis());
+ encodedProperties.put(Authorizable.LASTMODIFIED_BY_FIELD,
+ accessControlManager.getCurrentUserId());
+ }
+ encodedProperties.put(Authorizable.ID_FIELD, id); // make certain the ID is always there.
putCached(keySpace, authorizableColumnFamily, id, encodedProperties, authorizable.isNew());
authorizable.reset(getCached(keySpace, authorizableColumnFamily, id));
- storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, id, accessControlManager.getCurrentUserId(), true, type);
+ String[] attrs = attributes.toArray(new String[attributes.size()]);
+ storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, id, type, accessControlManager.getCurrentUserId(), wasNew, beforeUpdateProperties, attrs);
+ // for each added or removed member, send an UPDATE event so indexing can properly
+ // record the groups each member is a member of.\
+
+ // when we add members we dont emit an event with resource type in it.
+ if (membersAdded != null) {
+ for (String added : membersAdded) {
+ storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, added, accessControlManager.getCurrentUserId(), null, false, null);
+ }
+ }
+ if (membersRemoved != null) {
+ for (String removed : membersRemoved) {
+ storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, removed, accessControlManager.getCurrentUserId(), null, false, null);
+ }
+ }
}
+
public boolean createAuthorizable(String authorizableId, String authorizableName,
String password, Map properties) throws AccessDeniedException,
StorageClientException {
+ checkId(authorizableId);
if (properties == null) {
properties = Maps.newHashMap();
}
@@ -258,7 +342,7 @@ public boolean createAuthorizable(String authorizableId, String authorizableName
return false;
}
Map encodedProperties = StorageClientUtils.getFilteredAndEcodedMap(
- properties, FILTER_ON_CREATE);
+ properties, filterOnCreate);
encodedProperties.put(Authorizable.ID_FIELD, authorizableId);
encodedProperties
.put(Authorizable.NAME_FIELD, authorizableName);
@@ -269,14 +353,29 @@ public boolean createAuthorizable(String authorizableId, String authorizableName
encodedProperties.put(Authorizable.PASSWORD_FIELD,
Authorizable.NO_PASSWORD);
}
- encodedProperties.put(Authorizable.CREATED,
+ encodedProperties.put(Authorizable.CREATED_FIELD,
System.currentTimeMillis());
- encodedProperties.put(Authorizable.CREATED_BY,
+ encodedProperties.put(Authorizable.CREATED_BY_FIELD,
accessControlManager.getCurrentUserId());
putCached(keySpace, authorizableColumnFamily, authorizableId, encodedProperties, true);
return true;
}
+
+ private void checkId(String authorizableId) throws StorageClientException {
+ if ( authorizableId.charAt(0) == '_') {
+ throw new StorageClientException("Authorizables may not start with _ :"+authorizableId);
+ }
+ for ( int i = 0; i < authorizableId.length(); i++) {
+ int cp = authorizableId.codePointAt(i);
+ if ( Character.isWhitespace(cp) ||
+ Character.isISOControl(cp) ||
+ Character.isMirrored(cp) ) {
+ throw new StorageClientException("Authorizables may not contain :"+authorizableId.charAt(i));
+ }
+ }
+ }
+
public boolean createUser(String authorizableId, String authorizableName, String password,
Map properties) throws AccessDeniedException, StorageClientException {
if (properties == null) {
@@ -308,12 +407,39 @@ public boolean createGroup(String authorizableId, String authorizableName,
public void delete(String authorizableId) throws AccessDeniedException, StorageClientException {
checkOpen();
accessControlManager.check(Security.ZONE_ADMIN, authorizableId, Permissions.CAN_DELETE);
- removeFromCache(keySpace, authorizableColumnFamily, authorizableId);
- client.remove(keySpace, authorizableColumnFamily, authorizableId);
- storeListener.onDelete(Security.ZONE_AUTHORIZABLES, authorizableId, accessControlManager.getCurrentUserId());
+ Authorizable authorizable = findAuthorizable(authorizableId);
+ if (authorizable != null){
+ removeCached(keySpace, authorizableColumnFamily, authorizableId);
+ storeListener.onDelete(Security.ZONE_AUTHORIZABLES, authorizableId, accessControlManager.getCurrentUserId(), getType(authorizable), authorizable.getOriginalProperties());
+ }
+ }
+
+ private String getType(Authorizable authorizable) {
+ if ( authorizable != null ) {
+ if ( authorizable.hasProperty(Authorizable.AUTHORIZABLE_TYPE_FIELD)) {
+ return (String) authorizable.getProperty(Authorizable.AUTHORIZABLE_TYPE_FIELD);
+ } else if ( authorizable instanceof Group) {
+ return Authorizable.GROUP_VALUE;
+ } else if ( authorizable instanceof User) {
+ // this was an object.
+ return String.valueOf(Authorizable.USER_VALUE);
+ }
+
+ }
+ return null;
+ }
+ private String getType(Map props) {
+ if ( props != null ) {
+ if ( props.containsKey(Authorizable.AUTHORIZABLE_TYPE_FIELD)) {
+ return (String) props.get(Authorizable.AUTHORIZABLE_TYPE_FIELD);
+ }
+ }
+ return null;
}
+
+
public void close() {
closed = true;
}
@@ -333,19 +459,21 @@ public void changePassword(Authorizable authorizable, String password, String ol
if (!thisUser.isAdmin()) {
User u = authenticator.authenticate(id, oldPassword);
if (u == null) {
- throw new StorageClientException(
+ throw new IllegalArgumentException(
"Unable to change passwords, old password does not match");
}
}
putCached(keySpace, authorizableColumnFamily, id, ImmutableMap.of(
- Authorizable.LASTMODIFIED,
+ Authorizable.LASTMODIFIED_FIELD,
(Object)System.currentTimeMillis(),
- Authorizable.LASTMODIFIED_BY,
+ Authorizable.ID_FIELD,
+ id,
+ Authorizable.LASTMODIFIED_BY_FIELD,
accessControlManager.getCurrentUserId(),
Authorizable.PASSWORD_FIELD,
StorageClientUtils.secureHash(password)), false);
- storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, id, currentUserId, false, "op:change-password");
+ storeListener.onUpdate(Security.ZONE_AUTHORIZABLES, id, currentUserId, getType(authorizable), false, null, "op:change-password");
} else {
throw new AccessDeniedException(Security.ZONE_ADMIN, id,
@@ -355,7 +483,7 @@ public void changePassword(Authorizable authorizable, String password, String ol
}
- public Iterator findAuthorizable(String propertyName, String value,
+ public DisposableIterator findAuthorizable(String propertyName, String value,
Class extends Authorizable> authorizableType) throws StorageClientException {
Builder builder = ImmutableMap.builder();
if (value != null) {
@@ -366,13 +494,14 @@ public Iterator findAuthorizable(String propertyName, String value
} else if (authorizableType.equals(Group.class)) {
builder.put(Authorizable.AUTHORIZABLE_TYPE_FIELD, Authorizable.GROUP_VALUE);
}
- final Iterator