FELIX-3600: imported new UserAdmin contribution.
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1391437 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/useradmin/filestore/pom.xml b/useradmin/filestore/pom.xml
new file mode 100644
index 0000000..4cd68c2
--- /dev/null
+++ b/useradmin/filestore/pom.xml
@@ -0,0 +1,100 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <artifactId>felix-parent</artifactId>
+ <groupId>org.apache.felix</groupId>
+ <version>1.2.0</version>
+ <relativePath>../../../pom/pom.xml</relativePath>
+ </parent>
+ <artifactId>org.apache.felix.useradmin.filestore</artifactId>
+ <version>1.0.1</version>
+ <packaging>bundle</packaging>
+ <description>Provides a file-based repository store for the UserAdmin OSGi compendium service.</description>
+ <dependencies>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <version>4.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ <version>4.0.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.useradmin</artifactId>
+ <version>1.0.2</version>
+ <type>bundle</type>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Bundle-Name>Apache Felix User Admin file-based repository store</Bundle-Name>
+ <Bundle-Description>
+ A file-based repository store for User Admin Service of Apache Felix.
+ </Bundle-Description>
+ <Bundle-Activator>
+ ${project.artifactId}.osgi.Activator
+ </Bundle-Activator>
+ <Bundle-SymbolicName>
+ ${project.artifactId}
+ </Bundle-SymbolicName>
+ <Bundle-Vendor>The Apache Software Foundation</Bundle-Vendor>
+ <Import-Package>
+ org.osgi.service.useradmin; version="[1.1,1.2)",
+ org.apache.felix.useradmin; version="[1.0,1.1)",
+ *
+ </Import-Package>
+ <Export-Package>
+ </Export-Package>
+ <Private-Package>
+ ${project.artifactId}.*
+ </Private-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>rat-maven-plugin</artifactId>
+ <configuration>
+ <excludeSubProjects>false</excludeSubProjects>
+ <useEclipseDefaultExcludes>true</useEclipseDefaultExcludes>
+ <useMavenDefaultExcludes>true</useMavenDefaultExcludes>
+ <excludes>
+ <param>doc/*</param>
+ <param>maven-eclipse.xml</param>
+ <param>.checkstyle</param>
+ <param>.externalToolBuilders/*</param>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/ResettableTimer.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/ResettableTimer.java
new file mode 100644
index 0000000..b093e44
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/ResettableTimer.java
@@ -0,0 +1,114 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.filestore;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Provides a timer that can be reset.
+ */
+final class ResettableTimer {
+ private final ScheduledExecutorService m_executor;
+ private final Runnable m_task;
+ private final long m_timeout;
+ private final TimeUnit m_timeUnit;
+ private final AtomicReference m_futureRef;
+
+ /**
+ * Creates a new {@link ResettableTimer} calling a given task when a given
+ * timeout exceeds.
+ *
+ * @param task the task to execute upon timout, cannot be <code>null</code>;
+ * @param timeout the timeout value, > 0;
+ * @param unit the time unit of the timeout value, cannot be <code>null</code>.
+ */
+ public ResettableTimer(Runnable task, long timeout, TimeUnit unit) {
+ this(new ScheduledThreadPoolExecutor(1), task, timeout, unit);
+ }
+
+ /**
+ * Creates a new {@link ResettableTimer} calling a given task when a given
+ * timeout exceeds.
+ *
+ * @param executor the executor to use to execute the task, cannot be <code>null</code>;
+ * @param task the task to execute upon timout, cannot be <code>null</code>;
+ * @param timeout the timeout value, > 0;
+ * @param unit the time unit of the timeout value, cannot be <code>null</code>.
+ */
+ public ResettableTimer(ScheduledExecutorService executor, Runnable task, long timeout, TimeUnit unit) {
+ if (executor == null) {
+ throw new IllegalArgumentException("Executor cannot be null!");
+ }
+ if (task == null) {
+ throw new IllegalArgumentException("Task cannot be null!");
+ }
+ if (timeout <= 0) {
+ throw new IllegalArgumentException("Timeout cannot be negative!");
+ }
+ if (unit == null) {
+ throw new IllegalArgumentException("TimeUnit cannot be null!");
+ }
+
+ m_executor = executor;
+ m_task = task;
+ m_timeout = timeout;
+ m_timeUnit = unit;
+
+ m_futureRef = new AtomicReference();
+ }
+
+ /**
+ * Schedules the task for execution with the contained timeout. If a task
+ * is already pending or running, it will be cancelled (not interrupted).
+ * The new task will be scheduled to run in now + timeout.
+ */
+ public ScheduledFuture schedule() {
+ ScheduledFuture currentTask = cancelCurrentTask();
+ ScheduledFuture newTask = m_executor.schedule(m_task, m_timeout, m_timeUnit);
+ m_futureRef.compareAndSet(currentTask, newTask);
+ return newTask;
+ }
+
+ /**
+ * Shuts down this timer, allowing any pending tasks to execute. After this
+ * method is called, {@link #schedule()} may no longer be called.
+ */
+ public void shutDown() {
+ m_executor.shutdown();
+ try {
+ m_executor.awaitTermination(2 * m_timeout, m_timeUnit);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * @return the current task, or <code>null</code> if no task is available.
+ */
+ private ScheduledFuture cancelCurrentTask() {
+ ScheduledFuture currentTask = (ScheduledFuture) m_futureRef.get();
+ if (currentTask != null) {
+ // Doesn't matter for completed tasks...
+ currentTask.cancel(false /* mayInterruptIfRunning */);
+ }
+ return currentTask;
+ }
+}
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStore.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStore.java
new file mode 100644
index 0000000..115d223
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStore.java
@@ -0,0 +1,258 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.filestore;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+
+/**
+ * Provides an implementation of {@link RoleRepositoryStore} using Java Serialization.
+ */
+public class RoleRepositoryFileStore extends RoleRepositoryMemoryStore implements Runnable, UserAdminListener, ManagedService {
+
+ /** The PID for this service to allow its configuration to be updated. */
+ public static final String PID = "org.apache.felix.useradmin.rolerepositoryfilestore";
+
+ static final String KEY_WRITE_DISABLED = "background.write.disabled";
+ static final String KEY_WRITE_DELAY_VALUE = "background.write.delay.value";
+ static final String KEY_WRITE_DELAY_TIMEUNIT = "background.write.delay.timeunit";
+
+ private static final boolean DEFAULT_WRITE_DISABLED = Boolean.parseBoolean(System.getProperty(KEY_WRITE_DISABLED, "false"));
+ private static final int DEFAULT_WRITE_DELAY_VALUE = Integer.parseInt(System.getProperty(KEY_WRITE_DELAY_VALUE, "500"));
+ private static final TimeUnit DEFAULT_WRITE_DELAY_TIMEUNIT = TimeUnit.MILLISECONDS;
+
+ private static final String FILE_NAME = "ua_repo.dat";
+
+ private final File m_file;
+ private final AtomicReference m_timerRef;
+
+ /**
+ * Creates a new {@link RoleRepositoryStore} instance.
+ *
+ * @param baseDir the base directory where we can store our serialized data, cannot be <code>null</code>.
+ */
+ public RoleRepositoryFileStore(File baseDir) {
+ this(baseDir, !DEFAULT_WRITE_DISABLED);
+ }
+
+ /**
+ * Creates a new {@link RoleRepositoryStore} instance.
+ *
+ * @param baseDir the base directory where we can store our serialized data, cannot be <code>null</code>;
+ * @param backgroundWriteEnabled <code>true</code> if background writing should be enabled, <code>false</code> to disable it.
+ */
+ public RoleRepositoryFileStore(File baseDir, boolean backgroundWriteEnabled) {
+ m_file = new File(baseDir, FILE_NAME);
+
+ m_timerRef = new AtomicReference();
+
+ if (backgroundWriteEnabled) {
+ m_timerRef.set(new ResettableTimer(this, DEFAULT_WRITE_DELAY_VALUE, DEFAULT_WRITE_DELAY_TIMEUNIT));
+ }
+ }
+
+ public void initialize() throws IOException {
+ m_entries.putAll(retrieve());
+ }
+
+ public void roleChanged(UserAdminEvent event) {
+ scheduleTask();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Will be called by m_timer!</p>
+ */
+ public void run() {
+ try {
+ // Persist everything to disk...
+ flush();
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Stops this store service.
+ */
+ public void stop() {
+ ResettableTimer timer = (ResettableTimer) m_timerRef.get();
+ if (timer != null) {
+ // Shutdown and await termination...
+ timer.shutDown();
+ // Clear reference...
+ m_timerRef.compareAndSet(timer, null);
+ }
+
+ try {
+ // Write the latest version to disk...
+ flush();
+ }
+ catch (IOException e) {
+ // Nothing we can do about this here...
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void updated(Dictionary properties) throws ConfigurationException {
+ boolean writeDisabled = DEFAULT_WRITE_DISABLED;
+ int writeDelayValue = DEFAULT_WRITE_DELAY_VALUE;
+ TimeUnit writeDelayUnit = DEFAULT_WRITE_DELAY_TIMEUNIT;
+
+ if (properties != null) {
+ Object wd = properties.get(KEY_WRITE_DISABLED);
+ if (wd == null) {
+ throw new ConfigurationException(KEY_WRITE_DISABLED, "Missing write disabled value!");
+ }
+ try {
+ writeDisabled = Boolean.parseBoolean((String) wd);
+ } catch (Exception e) {
+ throw new ConfigurationException(KEY_WRITE_DISABLED, "Invalid write disabled value!");
+ }
+
+ if (!writeDisabled) {
+ Object wdv = properties.get(KEY_WRITE_DELAY_VALUE);
+ if (wdv == null) {
+ throw new ConfigurationException(KEY_WRITE_DELAY_VALUE, "Missing write delay value!");
+ }
+ try {
+ writeDelayValue = Integer.parseInt((String) wdv);
+ } catch (Exception e) {
+ throw new ConfigurationException(KEY_WRITE_DELAY_VALUE, "Invalid write delay value!");
+ }
+ if (writeDelayValue <= 0) {
+ throw new ConfigurationException(KEY_WRITE_DELAY_VALUE, "Invalid write delay value!");
+ }
+
+ Object wdu = properties.get(KEY_WRITE_DELAY_TIMEUNIT);
+ if (wdu != null) {
+ try {
+ writeDelayUnit = TimeUnit.valueOf(((String) wdu).toUpperCase());
+ } catch (Exception e) {
+ throw new ConfigurationException(KEY_WRITE_DELAY_TIMEUNIT, "Invalid write delay unit!");
+ }
+ }
+ }
+ }
+
+ ResettableTimer timer = (ResettableTimer) m_timerRef.get();
+ if (timer != null) {
+ timer.shutDown();
+ }
+ m_timerRef.compareAndSet(timer, writeDisabled ? null : new ResettableTimer(this, writeDelayValue, writeDelayUnit));
+ }
+
+ /**
+ * Retrieves the serialized repository from disk.
+ *
+ * @return the retrieved repository, never <code>null</code>.
+ * @throws IOException in case the retrieval of the repository failed.
+ */
+ protected Map retrieve() throws IOException {
+ InputStream is = null;
+
+ try {
+ is = new BufferedInputStream(new FileInputStream(m_file));
+
+ return new RoleRepositorySerializer().deserialize(is);
+ } catch (FileNotFoundException exception) {
+ // Don't bother; file does not exist...
+ return Collections.emptyMap();
+ } finally {
+ closeSafely(is);
+ }
+ }
+
+ /**
+ * Stores the given repository to disk as serialized objects.
+ *
+ * @param roleRepository the repository to store, cannot be <code>null</code>.
+ * @throws IOException in case storing the repository failed.
+ */
+ protected void store(Map roleRepository) throws IOException {
+ OutputStream os = null;
+
+ try {
+ os = new BufferedOutputStream(new FileOutputStream(m_file));
+
+ new RoleRepositorySerializer().serialize(roleRepository, os);
+ } finally {
+ closeSafely(os);
+ }
+ }
+
+ /**
+ * Closes a given resource, ignoring any exceptions that may come out of this.
+ *
+ * @param resource the resource to close, can be <code>null</code>.
+ */
+ private void closeSafely(Closeable resource) {
+ if (resource != null) {
+ try {
+ resource.close();
+ } catch (IOException e) {
+ // Ignore
+ }
+ }
+ }
+
+ /**
+ * Flushes the current repository to disk.
+ *
+ * @throws IOException in case of problems storing the repository.
+ */
+ private void flush() throws IOException {
+ store(new HashMap(m_entries));
+ }
+
+ /**
+ * Notifies the background timer to schedule a task for storing the
+ * contents of this store to disk.
+ */
+ private void scheduleTask() {
+ ResettableTimer timer = (ResettableTimer) m_timerRef.get();
+ if (timer != null) {
+ timer.schedule();
+ }
+ }
+}
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryMemoryStore.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryMemoryStore.java
new file mode 100644
index 0000000..f064f79
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryMemoryStore.java
@@ -0,0 +1,70 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.filestore;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.osgi.service.useradmin.Role;
+
+
+/**
+ * Provides a thread-safe in-memory role repository store.
+ */
+public class RoleRepositoryMemoryStore implements RoleRepositoryStore {
+
+ protected final ConcurrentMap m_entries = new ConcurrentHashMap();
+
+ public boolean addRole(Role role) throws IOException {
+ if (role == null) {
+ throw new IllegalArgumentException("Role cannot be null!");
+ }
+ Object result = m_entries.putIfAbsent(role.getName(), role);
+ return result == null;
+ }
+
+ public void close() throws IOException {
+ // Nop
+ }
+
+ public Role[] getAllRoles() throws IOException {
+ Collection roles = m_entries.values();
+ Role[] result = new Role[roles.size()];
+ return (Role[]) roles.toArray(result);
+ }
+
+ public Role getRoleByName(String roleName) throws IOException {
+ if (roleName == null) {
+ throw new IllegalArgumentException("Role name cannot be null!");
+ }
+ return (Role) m_entries.get(roleName);
+ }
+
+ public void initialize() throws IOException {
+ // Nop
+ }
+
+ public boolean removeRole(Role role) throws IOException {
+ if (role == null) {
+ throw new IllegalArgumentException("Role cannot be null!");
+ }
+ return m_entries.remove(role.getName(), role);
+ }
+}
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializer.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializer.java
new file mode 100644
index 0000000..57aca3e
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializer.java
@@ -0,0 +1,454 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.filestore;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+
+import org.apache.felix.useradmin.RoleFactory;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Provides a serializer for a role repository.
+ */
+final class RoleRepositorySerializer {
+
+ private static final int VALUE_TYPE_STRING = 1;
+ private static final int VALUE_TYPE_BARRAY = 2;
+
+ /**
+ * Deserializes a given input stream.
+ *
+ * @param is the input stream to deserialize, cannot be <code>null</code>.
+ * @return a {@link Map} representing the role repository. It provides a
+ * mapping between the name of the role as key and the associated
+ * role as value.
+ * @throws IOException in case of I/O problems;
+ * @throws IllegalArgumentException in case the given stream was <code>null</code>.
+ */
+ public Map deserialize(InputStream is) throws IOException {
+ if (is == null) {
+ throw new IllegalArgumentException("InputStream cannot be null!");
+ }
+ return readRepository(new DataInputStream(is));
+ }
+
+ /**
+ * Serializes a given map to the given output stream.
+ *
+ * @param roleRepository the repository to serialize, cannot be <code>null</code>;
+ * @param os the output stream to serialize to, cannot be <code>null</code>.
+ * @throws IOException in case of I/O problems;
+ * @throws IllegalArgumentException in case the given parameter was <code>null</code>.
+ */
+ public void serialize(Map roleRepository, OutputStream os) throws IOException {
+ if (roleRepository == null) {
+ throw new IllegalArgumentException("Map cannot be null!");
+ }
+ if (os == null) {
+ throw new IllegalArgumentException("OutputStream cannot be null!");
+ }
+ writeRepository(roleRepository, new DataOutputStream(os));
+ }
+
+ /**
+ * Adds all groups, based on the given stub groups.
+ *
+ * @param repository the repository to add the groups to, cannot be <code>null</code>;
+ * @param stubGroups the list with stub groups to replace, cannot be <code>null</code>.
+ * @throws IOException in case a referenced role was not found in the repository.
+ */
+ private void addGroups(Map repository, List stubGroups) throws IOException {
+ // First create "empty" groups in the repository; we'll fill them in later on...
+ Iterator sgIter = stubGroups.iterator();
+ while (sgIter.hasNext()) {
+ StubGroupImpl stubGroup = (StubGroupImpl) sgIter.next();
+
+ Group group = (Group) RoleFactory.createRole(Role.GROUP, stubGroup.getName());
+ copyDictionary(stubGroup.getProperties(), group.getProperties());
+ copyDictionary(stubGroup.getCredentials(), group.getCredentials());
+
+ repository.put(group.getName(), group);
+ }
+
+ int origSize = stubGroups.size();
+ while (!stubGroups.isEmpty()) {
+ List copy = new ArrayList(stubGroups);
+
+ int size = copy.size();
+ for (int i = 0; i < size; i++) {
+ StubGroupImpl stubGroup = (StubGroupImpl) copy.get(i);
+
+ Group group = (Group) repository.get(stubGroup.getName());
+ if (group != null) {
+ resolveGroupMembers(stubGroup, group, repository);
+ stubGroups.remove(stubGroup);
+ }
+ }
+
+ // In case we didn't resolve any groups; we should fail...
+ if (origSize == stubGroups.size()) {
+ throw new IOException("Failed to resolve groups: " + stubGroups);
+ }
+
+ origSize = stubGroups.size();
+ }
+ }
+
+ /**
+ * Converts a given {@link Dictionary} implementation to a {@link Map} implementation.
+ *
+ * @param dictionary the dictionary to convert, cannot be <code>null</code>.
+ * @return a {@link Map} instance with all the same key-value pairs as the given dictionary, never <code>null</code>.
+ */
+ private Map convertToMap(Dictionary dictionary) {
+ Map result = new HashMap();
+ if (dictionary instanceof Map) {
+ result.putAll((Map) dictionary);
+ } else {
+ Enumeration keyEnum = dictionary.keys();
+ while (keyEnum.hasMoreElements()) {
+ Object key = keyEnum.nextElement();
+ result.put(key, dictionary.get(key));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Copies the contents of a given dictionary to a given other dictionary.
+ *
+ * @param source the dictionary to copy from;
+ * @param dest the dictionary to copy to.
+ */
+ private void copyDictionary(Dictionary source, Dictionary dest) {
+ Enumeration keys = source.keys();
+ while (keys.hasMoreElements()) {
+ Object key = keys.nextElement();
+ Object value = source.get(key);
+ dest.put(key, value);
+ }
+ }
+
+ /**
+ * Reads and fills a given dictionary.
+ *
+ * @param dict the dictionary to read & fill, cannot be <code>null</code>;
+ * @param dis the input stream to read the data from, cannot be <code>null</code>.
+ * @throws IOException in case of I/O problems.
+ */
+ private void readDictionary(Dictionary dict, DataInputStream dis) throws IOException {
+ // Read the number of entries...
+ int count = dis.readInt();
+ while (count-- > 0) {
+ // Read the name of the key...
+ String key = dis.readUTF();
+ // Read the type of the value...
+ int type = dis.read();
+ // Read the value & add the actual entry...
+ if (VALUE_TYPE_BARRAY == type) {
+ int length = dis.readInt();
+ byte[] value = new byte[length];
+ if (dis.read(value, 0, length) != length) {
+ throw new IOException("Invalid repository; failed to correctly read dictionary!");
+ }
+ dict.put(key, value);
+ } else if (VALUE_TYPE_STRING == type) {
+ dict.put(key, dis.readUTF());
+ }
+ }
+ }
+
+ /**
+ * Reads a (stub) group from the given input stream.
+ *
+ * @param dis the input stream to read the data from, cannot be <code>null</code>.
+ * @return the read (stub) group, never <code>null</code>.
+ * @throws IOException in case of I/O problems.
+ */
+ private StubGroupImpl readGroup(DataInputStream dis) throws IOException {
+ StubGroupImpl group = new StubGroupImpl(dis.readUTF());
+
+ readDictionary(group.getProperties(), dis);
+ readDictionary(group.getCredentials(), dis);
+
+ // Read the number of basic members...
+ int count = dis.readInt();
+ while (count-- > 0) {
+ group.addMember(dis.readUTF());
+ }
+
+ // Read the number of required members...
+ count = dis.readInt();
+ while (count-- > 0) {
+ group.addRequiredMember(dis.readUTF());
+ }
+
+ return group;
+ }
+
+ /**
+ * Reads the entire repository from the given input stream.
+ *
+ * @param dis the input stream to read the data from, cannot be <code>null</code>.
+ * @return the repository {@link Map}, never <code>null</code>.
+ * @throws IOException in case of I/O problems.
+ */
+ private Map readRepository(DataInputStream dis) throws IOException {
+ Map repository = new HashMap();
+
+ int entryCount = dis.readInt();
+
+ List stubGroups = new ArrayList();
+
+ // Keep reading until no more types can be read...
+ while (entryCount-- > 0) {
+ int type = dis.readInt();
+
+ Role role = null;
+ if (Role.GROUP == type) {
+ stubGroups.add(readGroup(dis));
+ } else if (Role.USER == type) {
+ role = readUser(dis);
+ } else {
+ role = readRole(dis);
+ }
+
+ if (role != null) {
+ repository.put(role.getName(), role);
+ }
+ }
+
+ // Post processing stage: replace all stub groups with real group implementations...
+ addGroups(repository, stubGroups);
+
+ return repository;
+ }
+
+ /**
+ * Reads a role from the given input stream.
+ *
+ * @param dis the input stream to read the data from, cannot be <code>null</code>.
+ * @return the read role, never <code>null</code>.
+ * @throws IOException in case of I/O problems.
+ */
+ private Role readRole(DataInputStream dis) throws IOException {
+ Role role = RoleFactory.createRole(Role.ROLE, dis.readUTF());
+
+ readDictionary(role.getProperties(), dis);
+
+ return role;
+ }
+
+ /**
+ * Reads a user from the given input stream.
+ *
+ * @param dis the input stream to read the data from, cannot be <code>null</code>.
+ * @return the read user, never <code>null</code>.
+ * @throws IOException in case of I/O problems.
+ */
+ private User readUser(DataInputStream dis) throws IOException {
+ User user = (User) RoleFactory.createRole(Role.USER, dis.readUTF());
+
+ readDictionary(user.getProperties(), dis);
+ readDictionary(user.getCredentials(), dis);
+
+ return user;
+ }
+
+ /**
+ * Resolves all basic and required group members for a given group, based on the names from the given stub group.
+ *
+ * @param stubGroup the stub group to convert, cannot be <code>null</code>;
+ * @param repository the repository to take the roles from, cannot be <code>null</code>.
+ * @return a concrete {@link Group} instance with all members resolved, or <code>null</code> if not all members could be resolved.
+ * @throws IOException in case a referenced role was not found in the repository.
+ */
+ private void resolveGroupMembers(StubGroupImpl stubGroup, Group group, Map repository) throws IOException {
+ List names = stubGroup.getMemberNames();
+ int size = names.size();
+
+ for (int i = 0; i < size; i++) {
+ String name = (String) names.get(i);
+ Role role = (Role) repository.get(name);
+ if (role == null) {
+ throw new IOException("Unable to find referenced basic member: " + name);
+ }
+ group.addMember(role);
+ }
+
+ names = stubGroup.getRequiredMemberNames();
+ size = names.size();
+
+ for (int i = 0; i < size; i++) {
+ String name = (String) names.get(i);
+ Role role = (Role) repository.get(name);
+ if (role == null) {
+ throw new IOException("Unable to find referenced required member: " + name);
+ }
+ group.addRequiredMember(role);
+ }
+ }
+
+ /**
+ * Writes a given dictionary to the given output stream.
+ *
+ * @param dict the dictionary to write, cannot be <code>null</code>;
+ * @param dos the output stream to write the data to, cannot be <code>null</code>.
+ * @throws IOException in case of I/O problems.
+ */
+ private void writeDictionary(Dictionary dict, DataOutputStream dos) throws IOException {
+ Map properties = convertToMap(dict);
+
+ Set entries = properties.entrySet();
+ int size = entries.size();
+
+ // Write the number of entries...
+ dos.writeInt(size);
+
+ Iterator entriesIter = entries.iterator();
+ while (entriesIter.hasNext()) {
+ Map.Entry entry = (Entry) entriesIter.next();
+
+ dos.writeUTF((String) entry.getKey());
+
+ Object value = entry.getValue();
+ if (value instanceof String) {
+ dos.write(VALUE_TYPE_STRING);
+ dos.writeUTF((String) value);
+ } else if (value instanceof byte[]) {
+ dos.write(VALUE_TYPE_BARRAY);
+ dos.writeInt(((byte[]) value).length);
+ dos.write((byte[]) value);
+ }
+ }
+ }
+
+ /**
+ * Writes a given group to the given output stream.
+ *
+ * @param group the group to write, cannot be <code>null</code>.
+ * @param dos the output stream to write the data to, cannot be <code>null</code>.
+ * @throws IOException in case of I/O problems.
+ */
+ private void writeGroup(Group group, DataOutputStream dos) throws IOException {
+ dos.writeUTF(group.getName());
+
+ writeDictionary(group.getProperties(), dos);
+ writeDictionary(group.getCredentials(), dos);
+
+ Role[] m = group.getMembers();
+
+ if (m == null) {
+ dos.writeInt(0);
+ } else {
+ // Write the number of basic members...
+ dos.writeInt(m.length);
+ // Write the names of the basic members...
+ for (int i = 0; i < m.length; i++) {
+ dos.writeUTF(m[i].getName());
+ }
+ }
+
+ m = group.getRequiredMembers();
+
+ if (m == null) {
+ dos.writeInt(0);
+ } else {
+ // Write the number of required members...
+ dos.writeInt(m.length);
+ // Write the names of the required members...
+ for (int i = 0; i < m.length; i++) {
+ dos.writeUTF(m[i].getName());
+ }
+ }
+ }
+
+ /**
+ * Writes the given repository to the given output stream.
+ *
+ * @param repository the repository to write, cannot be <code>null</code>;
+ * @param dos the output stream to write the data to, cannot be <code>null</code>.
+ * @throws IOException in case of I/O problems.
+ */
+ private void writeRepository(Map repository, DataOutputStream dos) throws IOException {
+ Collection values = repository.values();
+ Iterator valuesIter = values.iterator();
+
+ // Write the total number of entries in our repository first...
+ dos.writeInt(values.size());
+
+ while (valuesIter.hasNext()) {
+ Role role = (Role) valuesIter.next();
+
+ int type = role.getType();
+
+ dos.writeInt(type);
+
+ if (Role.GROUP == type) {
+ writeGroup((Group) role, dos);
+ } else if (Role.USER == type) {
+ writeUser((User) role, dos);
+ } else {
+ writeRole(role, dos);
+ }
+ }
+ }
+
+ /**
+ * Writes a given role to the given output stream.
+ *
+ * @param role the role to write, cannot be <code>null</code>.
+ * @param dos the output stream to write the data to, cannot be <code>null</code>.
+ * @throws IOException in case of I/O problems.
+ */
+ private void writeRole(Role role, DataOutputStream dos) throws IOException {
+ dos.writeUTF(role.getName());
+
+ writeDictionary(role.getProperties(), dos);
+ }
+
+ /**
+ * Writes a given user to the given output stream.
+ *
+ * @param user the user to write, cannot be <code>null</code>.
+ * @param dos the output stream to write the data to, cannot be <code>null</code>.
+ * @throws IOException in case of I/O problems.
+ */
+ private void writeUser(User user, DataOutputStream dos) throws IOException {
+ dos.writeUTF(user.getName());
+
+ writeDictionary(user.getProperties(), dos);
+ writeDictionary(user.getCredentials(), dos);
+ }
+}
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/StubGroupImpl.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/StubGroupImpl.java
new file mode 100644
index 0000000..cb0a768
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/StubGroupImpl.java
@@ -0,0 +1,184 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.filestore;
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Properties;
+
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Provides a stub group that has "weak" references (based on their names) to its basic/required members.
+ */
+final class StubGroupImpl implements Group {
+
+ private final String m_name;
+ private final List m_basicMembers;
+ private final List m_requiredMembers;
+ private final Dictionary m_credentials;
+ private final Dictionary m_properties;
+
+ /**
+ * Creates a new {@link StubGroupImpl} instance.
+ */
+ public StubGroupImpl(String name) {
+ m_name = name;
+ m_properties = new Properties();
+ m_credentials = new Properties();
+ m_basicMembers = new ArrayList();
+ m_requiredMembers = new ArrayList();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean addMember(Role role) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean addMember(String roleName) {
+ return m_basicMembers.add(roleName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean addRequiredMember(Role role) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean addRequiredMember(String roleName) {
+ return m_requiredMembers.add(roleName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((m_name == null) ? 0 : m_name.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ StubGroupImpl other = (StubGroupImpl) obj;
+ if (m_name == null) {
+ if (other.m_name != null) {
+ return false;
+ }
+ } else if (!m_name.equals(other.m_name)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Dictionary getCredentials() {
+ return m_credentials;
+ }
+
+ /**
+ * Returns the names of all basic members.
+ *
+ * @return a list with basic member names (as String!), never <code>null</code>.
+ */
+ public List getMemberNames() {
+ return m_basicMembers;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Role[] getMembers() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getName() {
+ return m_name;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Dictionary getProperties() {
+ return m_properties;
+ }
+
+ /**
+ * Returns the names of all required members.
+ *
+ * @return a list with required member names (as String!), never <code>null</code>.
+ */
+ public List getRequiredMemberNames() {
+ return m_requiredMembers;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Role[] getRequiredMembers() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int getType() {
+ return Role.GROUP;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean hasCredential(String key, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean removeMember(Role role) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/osgi/Activator.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/osgi/Activator.java
new file mode 100644
index 0000000..edef63c
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/osgi/Activator.java
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.filestore.osgi;
+
+import java.util.Properties;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.apache.felix.useradmin.filestore.RoleRepositoryFileStore;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Registers the {@link RoleRepositoryFileStore} as {@link RoleRepositoryStore} service.
+ */
+public class Activator implements BundleActivator {
+
+ private RoleRepositoryFileStore m_store;
+
+ public void start(BundleContext context) throws Exception {
+ m_store = new RoleRepositoryFileStore(context.getDataFile(""));
+
+ String[] interfaces = { RoleRepositoryStore.class.getName(), UserAdminListener.class.getName(), ManagedService.class.getName() };
+
+ Properties props = new Properties();
+ props.put(Constants.SERVICE_PID, RoleRepositoryFileStore.PID);
+
+ context.registerService(interfaces, m_store, props);
+ }
+
+ public void stop(BundleContext context) throws Exception {
+ if (m_store != null) {
+ m_store.stop();
+ m_store = null;
+ }
+ }
+}
diff --git a/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/ResettableTimerTest.java b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/ResettableTimerTest.java
new file mode 100644
index 0000000..9193054
--- /dev/null
+++ b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/ResettableTimerTest.java
@@ -0,0 +1,201 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.filestore;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import junit.framework.TestCase;
+
+/**
+ * Test case for {@link ResettableTimer}.
+ */
+public class ResettableTimerTest extends TestCase {
+
+ private ResettableTimer m_timer;
+ private CountDownLatch m_latch;
+
+ /**
+ * Tests that a executor service is mandatory.
+ */
+ public void testExecutorSerivceIsMandatory() throws Exception {
+ Runnable task = createStubTask();
+
+ try {
+ new ResettableTimer(null, task, 10, TimeUnit.HOURS);
+ fail("IllegalArgumentException expected!");
+ } catch (Exception e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that multiple calls to {@link ResettableTimer#schedule()} causes
+ * pending tasks to be cancelled.
+ */
+ public void testScheduleCancelsPendingTasksOk() throws Exception {
+ final AtomicInteger m_counter = new AtomicInteger(0);
+
+ Runnable task = new Runnable() {
+ public void run() {
+ m_counter.incrementAndGet();
+ }
+ };
+
+ m_timer = new ResettableTimer(task, 100, TimeUnit.MILLISECONDS);
+ m_timer.schedule();
+
+ TimeUnit.MILLISECONDS.sleep(75);
+
+ Future f = m_timer.schedule();
+ f.get();
+
+ assertEquals(1, m_counter.get());
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.ResettableTimer#schedule()}.
+ */
+ public void testScheduleMultipleTasksOk() throws Exception {
+ final AtomicInteger m_counter = new AtomicInteger(0);
+
+ Runnable task = new Runnable() {
+ public void run() {
+ m_counter.incrementAndGet();
+ }
+ };
+
+ m_timer = new ResettableTimer(task, 100, TimeUnit.MILLISECONDS);
+
+ Future f = m_timer.schedule();
+ f.get();
+
+ f = m_timer.schedule();
+ f.get();
+
+ assertEquals(2, m_counter.get());
+ }
+
+ /**
+ * Tests that a task is invoked as single shot.
+ */
+ public void testScheduleSingleShotOk() throws Exception {
+ m_latch = new CountDownLatch(1);
+
+ Runnable task = new Runnable() {
+ public void run() {
+ m_latch.countDown();
+ }
+ };
+
+ m_timer = new ResettableTimer(task, 100, TimeUnit.MILLISECONDS);
+ m_timer.schedule();
+
+ assertTrue(m_latch.await(200, TimeUnit.MILLISECONDS));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.ResettableTimer#shutDown()}.
+ */
+ public void testShutDownOk() throws Exception {
+ final AtomicInteger m_counter = new AtomicInteger(0);
+
+ Runnable task = new Runnable() {
+ public void run() {
+ m_counter.incrementAndGet();
+ }
+ };
+
+ m_timer = new ResettableTimer(task, 100, TimeUnit.MILLISECONDS);
+ m_timer.schedule();
+
+ TimeUnit.MILLISECONDS.sleep(75);
+
+ m_timer.schedule();
+ m_timer.shutDown();
+
+ assertEquals(1, m_counter.get());
+ }
+
+ /**
+ * Tests that a task is mandatory.
+ */
+ public void testTaskIsMandatory() throws Exception {
+ try {
+ new ResettableTimer(null, 10, TimeUnit.HOURS);
+ fail("IllegalArgumentException expected!");
+ } catch (Exception e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that a timeout cannot be zero.
+ */
+ public void testTimeoutCannotBeNegative() throws Exception {
+ Runnable task = createStubTask();
+
+ try {
+ new ResettableTimer(task, -1, TimeUnit.MILLISECONDS);
+ fail("IllegalArgumentException expected!");
+ } catch (Exception e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that a timeout cannot be zero.
+ */
+ public void testTimeoutCannotBeZero() throws Exception {
+ Runnable task = createStubTask();
+
+ try {
+ new ResettableTimer(task, 0, TimeUnit.MILLISECONDS);
+ fail("IllegalArgumentException expected!");
+ } catch (Exception e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that a time unit is mandatory.
+ */
+ public void testTimeUnitIsMandatory() throws Exception {
+ Runnable task = createStubTask();
+
+ try {
+ new ResettableTimer(task, 10, null);
+ fail("IllegalArgumentException expected!");
+ } catch (Exception e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * @return a {@link Runnable} that does nothing, never <code>null</code>.
+ */
+ private Runnable createStubTask() {
+ return new Runnable() {
+ public void run() {
+ // No-op
+ }
+ };
+ }
+}
diff --git a/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStorePerformanceTest.java b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStorePerformanceTest.java
new file mode 100644
index 0000000..c19699f
--- /dev/null
+++ b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStorePerformanceTest.java
@@ -0,0 +1,168 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.filestore;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.useradmin.impl.RoleRepository;
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Test case for {@link RoleRepositorySerializer}.
+ */
+public class RoleRepositoryFileStorePerformanceTest extends TestCase {
+
+ private static final int USER_COUNT = 15000;
+ private static final int GROUP_COUNT = 500;
+
+ private Group[] m_groups;
+ private User[] m_users;
+
+ private Map m_repository;
+ private RoleRepositoryFileStore m_store;
+
+ /**
+ * Executes the performance test.
+ */
+ public void testPerformanceOk() throws Exception {
+ allocateMemory();
+
+ writeRepositoryPerformanceTest();
+
+ releaseMemory();
+
+ readRepositoryPerformanceTest();
+ }
+
+ /**
+ * Does a very simple performance test for a large number of users spread over several groups.
+ */
+ protected void readRepositoryPerformanceTest() throws Exception {
+ long r_st = System.currentTimeMillis();
+ Map result = m_store.retrieve();
+ long r_time = System.currentTimeMillis() - r_st;
+
+ assertNotNull(result);
+ assertEquals(GROUP_COUNT + USER_COUNT + 1, result.size());
+
+ System.out.println("Read time : " + (r_time / 1000.0) + "s.");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_store = new RoleRepositoryFileStore(new File(System.getProperty("java.io.tmpdir")), false /* disable background writes */);
+
+ m_repository = new HashMap(USER_COUNT + GROUP_COUNT + 1);
+
+ addToRepository(RoleRepository.USER_ANYONE);
+ }
+
+ /**
+ * Does a very simple performance test for writing a large number of users spread over several groups.
+ */
+ protected void writeRepositoryPerformanceTest() throws Exception {
+ long w_st = System.currentTimeMillis();
+ m_store.store(m_repository);
+ long w_time = System.currentTimeMillis() - w_st;
+
+ System.out.println("Write time: " + (w_time / 1000.0) + "s.");
+ }
+
+ private void addToRepository(Role role) {
+ m_repository.put(role.getName(), role);
+ }
+
+ /**
+ *
+ */
+ private void allocateMemory() {
+ m_groups = new Group[GROUP_COUNT];
+ for (int i = 0; i < m_groups.length; i++) {
+ m_groups[i] = createGroup(i+1);
+ m_groups[i].addRequiredMember(RoleRepository.USER_ANYONE);
+
+ addToRepository(m_groups[i]);
+ }
+
+ m_users = new User[USER_COUNT];
+ for (int i = 0; i < m_users.length; i++) {
+ m_users[i] = createUser(i+1);
+
+ int groupIdx = (i % m_groups.length);
+ m_groups[groupIdx].addMember(m_users[i]);
+
+ addToRepository(m_users[i]);
+ }
+ }
+
+ private Group createGroup(int idx) {
+ String name = "Group" + idx;
+
+ Group result = new GroupImpl(name);
+
+ setCredentials(result);
+ setProperties(result);
+
+ return result;
+ }
+
+ private User createUser(int idx) {
+ String name = "User" + idx;
+
+ User result = new UserImpl(name);
+
+ setCredentials(result);
+ setProperties(result);
+
+ return result;
+ }
+
+ /**
+ *
+ */
+ private void releaseMemory() {
+ m_groups = null;
+ m_users = null;
+
+ System.gc();
+ System.gc();
+ System.gc();
+ }
+
+ private void setCredentials(User user) {
+ user.getCredentials().put("name", user.getName());
+ user.getCredentials().put("password", "secret");
+ user.getCredentials().put("certificate", new byte[] { (byte) 0x55, (byte) 0xAA } );
+ }
+
+ private void setProperties(Role role) {
+ role.getProperties().put("key1", "value1");
+ role.getProperties().put("key2", "hello world".getBytes());
+ }
+}
diff --git a/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStoreTest.java b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStoreTest.java
new file mode 100644
index 0000000..30754bf
--- /dev/null
+++ b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStoreTest.java
@@ -0,0 +1,189 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.filestore;
+
+import java.io.File;
+import java.util.Properties;
+
+import junit.framework.TestCase;
+
+import org.osgi.service.cm.ConfigurationException;
+
+/**
+ * Test cases for {@link RoleRepositoryFileStore}.
+ */
+public class RoleRepositoryFileStoreTest extends TestCase {
+
+ private RoleRepositoryFileStore m_store;
+
+ /**
+ * Tests that calling updated without the key "background.write.disabled" fails.
+ */
+ public void testUpdateConfigurationWithoutKeyWriteDisabledFail() throws Exception {
+ try {
+ m_store.updated(new Properties());
+ fail("ConfigurationException expected!");
+ } catch (ConfigurationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that calling updated with the key "background.write.disabled" set to "false" succeeds.
+ */
+ public void testUpdateConfigurationWithKeyWriteDisabledOk() throws Exception {
+ Properties properties = new Properties();
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "true");
+
+ m_store.updated(properties);
+ }
+
+ /**
+ * Tests that calling updated with the key "background.write.disabled" set to a numeric value fails.
+ */
+ public void testUpdateConfigurationWithKeyWriteDisabledInvalidValueFail() throws Exception {
+ Properties properties = new Properties();
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, Integer.valueOf(1));
+
+ try {
+ m_store.updated(properties);
+ fail("ConfigurationException expected!");
+ } catch (ConfigurationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that calling updated without the key "background.write.delay.value" fails.
+ */
+ public void testUpdateConfigurationWithoutKeyWriteDelayValueFail() throws Exception {
+ Properties properties = new Properties();
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "seconds");
+
+ try {
+ m_store.updated(properties);
+ fail("ConfigurationException expected!");
+ } catch (ConfigurationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that calling updated with the key "background.write.delay.value" set to a non-numeric value fails.
+ */
+ public void testUpdateConfigurationWithKeyWriteDelayValueInvalidValueTypeFail() throws Exception {
+ Properties properties = new Properties();
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "seconds");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "seconds");
+
+ try {
+ m_store.updated(properties);
+ fail("ConfigurationException expected!");
+ } catch (ConfigurationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that calling updated with the key "background.write.delay.value" set to zero fails.
+ */
+ public void testUpdateConfigurationWithKeyWriteDelayValueZeroValueFail() throws Exception {
+ Properties properties = new Properties();
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "0");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "seconds");
+
+ try {
+ m_store.updated(properties);
+ fail("ConfigurationException expected!");
+ } catch (ConfigurationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that calling updated with the key "background.write.delay.value" set to a negative value fails.
+ */
+ public void testUpdateConfigurationWithKeyWriteDelayValueNegativeValueFail() throws Exception {
+ Properties properties = new Properties();
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "-1");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "seconds");
+
+ try {
+ m_store.updated(properties);
+ fail("ConfigurationException expected!");
+ } catch (ConfigurationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that calling updated without the key "background.write.delay.timeunit" succeeds as it is optional.
+ */
+ public void testUpdateConfigurationWithoutKeyWriteDelayTimeUnitOk() throws Exception {
+ Properties properties = new Properties();
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "1");
+
+ m_store.updated(properties);
+ }
+
+ /**
+ * Tests that calling updated with the key "background.write.delay.timeunit" set to an invalid value fails.
+ */
+ public void testUpdateConfigurationWithKeyWriteDelayTimeUnitInvalidValueFail() throws Exception {
+ Properties properties = new Properties();
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "1");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "1");
+
+ try {
+ m_store.updated(properties);
+ fail("ConfigurationException expected!");
+ } catch (ConfigurationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that calling updated with all keys succeeds.
+ */
+ public void testUpdateConfigurationWithAllKeysOk() throws Exception {
+ Properties properties = new Properties();
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "1");
+ properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "seconds");
+
+ m_store.updated(properties);
+ }
+
+ /**
+ * Tests that calling updated with a <code>null</code>-dictionary causes the default settings to be applied.
+ */
+ public void testUpdateConfigurationWithoutPropertiesOk() throws Exception {
+ m_store.updated(null);
+ }
+
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_store = new RoleRepositoryFileStore(new File(System.getProperty("java.io.tmpdir")), false /* disable background writes */);
+ }
+}
diff --git a/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializerTest.java b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializerTest.java
new file mode 100644
index 0000000..5f1aadb
--- /dev/null
+++ b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializerTest.java
@@ -0,0 +1,413 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.filestore;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.useradmin.impl.RoleRepository;
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Test case for {@link RoleRepositorySerializer}.
+ */
+public class RoleRepositorySerializerTest extends TestCase {
+
+ private Role m_userAnyone;
+ private User m_user1;
+ private User m_user2;
+ private User m_user3;
+ private User m_user4;
+ private Group m_group1;
+ private Group m_group2;
+ private Group m_group3;
+ private Map m_repository;
+
+ private RoleRepositorySerializer m_serializer;
+
+ /**
+ * Tests that writing and reading a repository with a role, four users and a three group works as expected.
+ */
+ public void testRWRepositoryWithRolesUsersAndGroupsOk() throws Exception {
+ m_group1.addRequiredMember(m_userAnyone);
+ m_group1.addMember(m_user1);
+
+ m_group2.addMember(m_group1);
+ m_group2.addMember(m_user2);
+ m_group2.addRequiredMember(m_user1);
+
+ m_group3.addMember(m_user3);
+ m_group3.addMember(m_user4);
+ m_group3.addRequiredMember(m_userAnyone);
+
+ addToRepository(m_userAnyone);
+ addToRepository(m_user1);
+ addToRepository(m_user2);
+ addToRepository(m_user3);
+ addToRepository(m_user4);
+ addToRepository(m_group1);
+ addToRepository(m_group2);
+ addToRepository(m_group3);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ m_serializer.serialize(m_repository, dos); // should succeed!
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ DataInputStream dis = new DataInputStream(bais);
+
+ Map result = m_serializer.deserialize(dis);
+ assertNotNull(result);
+
+ assertEquals(8, result.size());
+ assertEquals(m_userAnyone, (Role) result.get(m_userAnyone.getName()));
+ assertEquals(m_user1, (User) result.get(m_user1.getName()));
+ assertEquals(m_user2, (User) result.get(m_user2.getName()));
+ assertEquals(m_user3, (User) result.get(m_user3.getName()));
+ assertEquals(m_user4, (User) result.get(m_user4.getName()));
+ assertEquals(m_group1, (Group) result.get(m_group1.getName()));
+ assertEquals(m_group2, (Group) result.get(m_group2.getName()));
+ assertEquals(m_group3, (Group) result.get(m_group3.getName()));
+ }
+
+ /**
+ * Tests that writing and reading a repository with a single group works as expected.
+ */
+ public void testRWRepositoryWithSingleGroupOk() throws Exception {
+ addToRepository(m_group1);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ m_serializer.serialize(m_repository, dos); // should succeed!
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ DataInputStream dis = new DataInputStream(bais);
+
+ Map result = m_serializer.deserialize(dis);
+ assertNotNull(result);
+
+ assertEquals(1, result.size());
+
+ assertEquals(m_group1, (Role) result.get(m_group1.getName()));
+ }
+
+ /**
+ * Tests that writing and reading a repository with a single role works as expected.
+ */
+ public void testRWRepositoryWithSingleRoleOk() throws Exception {
+ addToRepository(m_userAnyone);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ m_serializer.serialize(m_repository, dos); // should succeed!
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ DataInputStream dis = new DataInputStream(bais);
+
+ Map result = m_serializer.deserialize(dis);
+ assertNotNull(result);
+
+ assertEquals(1, result.size());
+
+ assertEquals(m_userAnyone, (Role) result.get(m_userAnyone.getName()));
+ }
+
+ /**
+ * Tests that writing and reading a repository with a single user role works as expected.
+ */
+ public void testRWRepositoryWithSingleUserOk() throws Exception {
+ addToRepository(m_user1);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ m_serializer.serialize(m_repository, dos); // should succeed!
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ DataInputStream dis = new DataInputStream(bais);
+
+ Map result = m_serializer.deserialize(dis);
+ assertNotNull(result);
+
+ assertEquals(1, result.size());
+
+ assertEquals(m_user1, (User) result.get(m_user1.getName()));
+ }
+
+ /**
+ * Tests that writing and reading a repository with two user roles works as expected.
+ */
+ public void testRWRepositoryWithTwoUsersOk() throws Exception {
+ addToRepository(m_user1);
+ addToRepository(m_user2);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ m_serializer.serialize(m_repository, dos); // should succeed!
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ DataInputStream dis = new DataInputStream(bais);
+
+ Map result = m_serializer.deserialize(dis);
+ assertNotNull(result);
+
+ assertEquals(2, result.size());
+ assertEquals(m_user1, (User) result.get(m_user1.getName()));
+ assertEquals(m_user2, (User) result.get(m_user2.getName()));
+ }
+
+ /**
+ * Tests that reading a repository that has a cyclic group reference can be read ok.
+ */
+ public void testRWRepositoryWithCyclicGroupReference() throws Exception {
+ m_group1.addMember(m_group2);
+ m_group2.addMember(m_group1);
+ m_group2.addMember(m_user1);
+
+ addToRepository(m_user1);
+ addToRepository(m_group2);
+ addToRepository(m_group1);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ m_serializer.serialize(m_repository, dos); // should succeed!
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ DataInputStream dis = new DataInputStream(bais);
+
+ Map result = m_serializer.deserialize(dis);
+ assertNotNull(result);
+
+ assertEquals(3, result.size());
+ assertEquals(m_group1, (Group) result.get(m_group1.getName()));
+ assertEquals(m_group2, (Group) result.get(m_group2.getName()));
+ assertEquals(m_user1, (User) result.get(m_user1.getName()));
+ }
+
+ /**
+ * Tests that reading a repository that is missing a referenced basic role from a group fails.
+ */
+ public void testRWRepositoryWithUnreferencedBasicRoleInGroupFail() throws Exception {
+ m_group1.addMember(m_userAnyone);
+ m_group1.addRequiredMember(m_user1);
+
+ // "Forget" to add the user.anyone!
+ addToRepository(m_user1);
+ addToRepository(m_group1);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ m_serializer.serialize(m_repository, dos); // should succeed!
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ DataInputStream dis = new DataInputStream(bais);
+
+ try {
+ m_serializer.deserialize(dis);
+
+ fail("IOException expected!");
+ } catch (IOException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that reading a repository that is missing a referenced required role from a group fails.
+ */
+ public void testRWRepositoryWithUnreferencedRequiredRoleInGroupFail() throws Exception {
+ m_group1.addRequiredMember(m_userAnyone);
+ m_group1.addMember(m_user1);
+
+ // "Forget" to add the user.anyone!
+ addToRepository(m_user1);
+ addToRepository(m_group1);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ DataOutputStream dos = new DataOutputStream(baos);
+
+ m_serializer.serialize(m_repository, dos); // should succeed!
+
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ DataInputStream dis = new DataInputStream(bais);
+
+ try {
+ m_serializer.deserialize(dis);
+
+ fail("IOException expected!");
+ } catch (IOException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_serializer = new RoleRepositorySerializer();
+
+ m_repository = new HashMap();
+
+ m_userAnyone = RoleRepository.USER_ANYONE;
+
+ setProperties(m_userAnyone);
+
+ m_user1 = createUser(1);
+ m_user2 = createUser(2);
+ m_user3 = createUser(3);
+ m_user4 = createUser(4);
+
+ m_group1 = createGroup(1);
+ m_group2 = createGroup(2);
+ m_group3 = createGroup(3);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ private void addToRepository(Role role) {
+ m_repository.put(role.getName(), role);
+ }
+
+ private void assertEquals(Dictionary expected, Dictionary obtained) {
+ assertNotNull(expected);
+ assertNotNull(obtained);
+
+ assertEquals(expected.size(), obtained.size());
+
+ Enumeration enumeration = expected.keys();
+ while (enumeration.hasMoreElements()) {
+ Object expectedKey = enumeration.nextElement();
+ Object expectedValue = expected.get(expectedKey);
+
+ Object obtainedValue = obtained.get(expectedKey);
+ if (expectedValue == null) {
+ assertNull(obtainedValue);
+ } else if (expectedValue.getClass().isArray()) {
+ assertTrue(Arrays.equals((byte[]) expectedValue, (byte[]) obtainedValue));
+ } else {
+ assertEquals(expectedValue, obtainedValue);
+ }
+ }
+ }
+
+ private void assertEquals(Group expected, Group obtained) {
+ assertEquals((Object) expected, (Object) obtained);
+
+ assertEquals(expected.getProperties(), obtained.getProperties());
+ assertEquals(expected.getCredentials(), obtained.getCredentials());
+ assertEquals(expected.getMembers(), obtained.getMembers());
+ assertEquals(expected.getRequiredMembers(), obtained.getRequiredMembers());
+ }
+
+ private void assertEquals(Role expected, Role obtained) {
+ assertEquals((Object) expected, (Object) obtained);
+
+ assertEquals(expected.getProperties(), obtained.getProperties());
+ }
+
+ private void assertEquals(User expected, User obtained) {
+ assertEquals((Object) expected, (Object) obtained);
+
+ assertEquals(expected.getProperties(), obtained.getProperties());
+ assertEquals(expected.getCredentials(), obtained.getCredentials());
+ }
+
+ private void assertEquals(Role[] expected, Role[] obtained) {
+ if (expected != null) {
+ assertNotNull(obtained);
+ assertEquals(expected.length, obtained.length);
+
+ List e = new ArrayList(Arrays.asList(expected));
+ e.removeAll(Arrays.asList(obtained));
+
+ assertTrue("Roles not obtained: " + e, e.isEmpty());
+
+ Map m = new HashMap();
+ for (int i = 0; i < expected.length; i++) {
+ m.put(expected[i].getName(), expected[i]);
+ }
+
+ for (int i = 0; i < obtained.length; i++) {
+ assertEquals(m.get(obtained[i].getName()), obtained[i]);
+ }
+ } else {
+ assertNull(obtained);
+ }
+ }
+
+ private User createUser(int idx) {
+ String name = "User" + idx;
+
+ User result = new UserImpl(name);
+
+ setCredentials(result);
+ setProperties(result);
+
+ return result;
+ }
+
+ private Group createGroup(int idx) {
+ String name = "Group" + idx;
+
+ Group result = new GroupImpl(name);
+
+ setCredentials(result);
+ setProperties(result);
+
+ return result;
+ }
+
+ private void setCredentials(User user) {
+ user.getCredentials().put(user.getName(), user.getName());
+ user.getCredentials().put("password", user.getName());
+ user.getCredentials().put("certificate", new byte[] { (byte) 0x55, (byte) 0xAA } );
+ }
+
+ private void setProperties(Role role) {
+ role.getProperties().put(role.getName(), role.getName());
+ role.getProperties().put("key1", role.getName());
+ role.getProperties().put("key2", "hello world".getBytes());
+ }
+}
diff --git a/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/StubGroupImplTest.java b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/StubGroupImplTest.java
new file mode 100644
index 0000000..b19d53e
--- /dev/null
+++ b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/StubGroupImplTest.java
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.filestore;
+
+import junit.framework.TestCase;
+
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Test cases for {@link StubGroupImpl}.
+ */
+public class StubGroupImplTest extends TestCase {
+
+ /**
+ * Tests that {@link StubGroupImpl#addMember(Role)} always fails.
+ */
+ public void testAddMemberRoleAlwaysFails() {
+ try {
+ new StubGroupImpl("test").addMember((Role) null);
+ fail("Expected UnsupportedOperationException!");
+ } catch (UnsupportedOperationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that {@link StubGroupImpl#addRequiredMember(Role)} always fails.
+ */
+ public void testAddRequiredMemberRoleAlwaysFails() {
+ try {
+ new StubGroupImpl("test").addRequiredMember((Role) null);
+ fail("Expected UnsupportedOperationException!");
+ } catch (UnsupportedOperationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that {@link StubGroupImpl#getMembers()} always fails.
+ */
+ public void testGetMembersAlwaysFails() {
+ try {
+ new StubGroupImpl("test").getMembers();
+ fail("Expected UnsupportedOperationException!");
+ } catch (UnsupportedOperationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that {@link StubGroupImpl#getRequiredMembers()} always fails.
+ */
+ public void testGetRequiredMembersAlwaysFails() {
+ try {
+ new StubGroupImpl("test").getRequiredMembers();
+ fail("Expected UnsupportedOperationException!");
+ } catch (UnsupportedOperationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that {@link StubGroupImpl#hasCredential(String, Object)} always fails.
+ */
+ public void testHasCredentialAlwaysFails() {
+ try {
+ new StubGroupImpl("test").hasCredential("foo", "bar");
+ fail("Expected UnsupportedOperationException!");
+ } catch (UnsupportedOperationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that {@link StubGroupImpl#removeMember(Role)} always fails.
+ */
+ public void testRemoveMemberAlwaysFails() {
+ try {
+ new StubGroupImpl("test").removeMember((Role) null);
+ fail("Expected UnsupportedOperationException!");
+ } catch (UnsupportedOperationException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that {@link StubGroupImpl#hashCode()} and {@link StubGroupImpl#equals(Object)} works.
+ */
+ public void testHashCodeAndEqualsOk() {
+ StubGroupImpl g1 = new StubGroupImpl("foo");
+ StubGroupImpl g2 = new StubGroupImpl("bar");
+ StubGroupImpl g3 = new StubGroupImpl("foo");
+ StubGroupImpl g4 = new StubGroupImpl(null);
+
+ assertTrue(g1.hashCode() == g3.hashCode());
+ assertFalse(g1.hashCode() == g2.hashCode());
+ assertFalse(g1.hashCode() == g4.hashCode());
+
+ assertTrue(g1.equals(g3));
+ assertTrue(g3.equals(g1));
+ assertFalse(g1.equals(g2));
+ assertFalse(g1.equals(g4));
+
+ assertTrue(g4.equals(g4));
+ assertFalse(g4.equals(g1));
+
+ assertFalse(g1.equals(null));
+ assertFalse(g1.equals("qux"));
+ }
+}
diff --git a/useradmin/mongodb/pom.xml b/useradmin/mongodb/pom.xml
new file mode 100644
index 0000000..1d110b1
--- /dev/null
+++ b/useradmin/mongodb/pom.xml
@@ -0,0 +1,93 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <artifactId>felix-parent</artifactId>
+ <groupId>org.apache.felix</groupId>
+ <version>1.2.0</version>
+ <relativePath>../../../pom/pom.xml</relativePath>
+ </parent>
+ <artifactId>org.apache.felix.useradmin.mongodb</artifactId>
+ <version>1.0.0</version>
+ <packaging>bundle</packaging>
+ <description>Provides a MongoDB store implementation for the User Admin service.</description>
+ <dependencies>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <version>4.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ <version>4.0.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.mongodb</groupId>
+ <artifactId>mongo-java-driver</artifactId>
+ <version>2.8.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.useradmin</artifactId>
+ <version>1.0.1</version>
+ <type>bundle</type>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.6</source>
+ <target>1.6</target>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Bundle-Name>Apache Felix User Admin Service MongoDB repository</Bundle-Name>
+ <Bundle-Description>
+ A MongoDB repository implementation for User Admin Compendium Service
+ </Bundle-Description>
+ <Bundle-Activator>
+ ${project.artifactId}.osgi.Activator
+ </Bundle-Activator>
+ <Bundle-SymbolicName>
+ ${project.artifactId}
+ </Bundle-SymbolicName>
+ <Bundle-Vendor>The Apache Software Foundation</Bundle-Vendor>
+ <Import-Package>
+ org.osgi.service.useradmin; version="[1.1,1.2)",
+ *
+ </Import-Package>
+ <Export-Package>
+ </Export-Package>
+ <Private-Package>
+ ${project.artifactId}.*
+ </Private-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>rat-maven-plugin</artifactId>
+ <configuration>
+ <excludeSubProjects>false</excludeSubProjects>
+ <useEclipseDefaultExcludes>true</useEclipseDefaultExcludes>
+ <useMavenDefaultExcludes>true</useMavenDefaultExcludes>
+ <excludes>
+ <param>doc/*</param>
+ <param>maven-eclipse.xml</param>
+ <param>.checkstyle</param>
+ <param>.externalToolBuilders/*</param>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
\ No newline at end of file
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/KeyCodec.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/KeyCodec.java
new file mode 100644
index 0000000..fe6cdec
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/KeyCodec.java
@@ -0,0 +1,143 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.mongodb;
+
+
+/**
+ * Provides a small utility class for encoding/decoding keys used directly in
+ * MongoDB.
+ * <p>
+ * MongoDB does not allow keys to have '.' and '$' (amongst others?) in their
+ * name, so we need to ensure that those characters are somehow encoded.
+ * </p>
+ *
+ * @see http://www.mongodb.org/display/DOCS/Legal+Key+Names
+ * @see http://www.yoyobrain.com/flashcards/show/261984
+ */
+final class KeyCodec {
+
+ private static final String ENCODED_NULL = "%00";
+ private static final String ENCODED_UNDERSCORE = "%5F";
+ private static final String ENCODED_DOT = "%2E";
+ private static final String ENCODED_DOLLAR = "%24";
+
+ /**
+ * Encodes a given key by replacing all '.' and '$' with their URL encoded variants.
+ *
+ * @param input the input to encode, may be <code>null</code>.
+ * @return the encoded input, can be <code>null</code>.
+ */
+ public static String encode(String input) {
+ if (input == null) {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder(input.length());
+ for (int i = 0; i < input.length(); i++) {
+ char c = input.charAt(i);
+ if (c == 0) {
+ sb.append(ENCODED_NULL);
+ } else if (c == '_') {
+ sb.append(ENCODED_UNDERSCORE);
+ } else if (c == '.') {
+ sb.append(ENCODED_DOT);
+ } else if (c == '$') {
+ sb.append(ENCODED_DOLLAR);
+ } else if (c == '%') {
+ // escape all '%' as well...
+ sb.append("%%");
+ } else {
+ sb.append((char) c);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Decodes a given key by replacing all URL encoded '.' and '$' entities with their real characters.
+ *
+ * @param input the input to decode, may be <code>null</code>.
+ * @return the decoded input, can be <code>null</code>.
+ */
+ public static String decode(String input) {
+ if (input == null) {
+ return null;
+ }
+
+ boolean percentSeen = false;
+ int length = input.length();
+ char oldC = input.charAt(0);
+
+ StringBuilder sb = new StringBuilder(input.length());
+ for (int i = 1; i < length; i++) {
+ char c = input.charAt(i);
+ if (oldC == '%') {
+ if (c == '%') {
+ // Escaped percent...
+ sb.append("%");
+ c = 0; // erase percent...
+ percentSeen = false;
+ } else {
+ percentSeen = true;
+ }
+ } else if (c == '%') {
+ percentSeen = true;
+ } else if (percentSeen) {
+ if (oldC == '0' && c == '0') {
+ // Encoded null-character
+ sb.append((char) 0);
+ percentSeen = false;
+ } else if (oldC == '2' && c == '4') {
+ // Encoded dollar...
+ sb.append("$");
+ percentSeen = false;
+ } else if (oldC == '2' && c == 'E') {
+ // Encoded dot...
+ sb.append(".");
+ percentSeen = false;
+ } else if (oldC == '5' && c == 'F') {
+ // Encoded underscore...
+ sb.append("_");
+ percentSeen = false;
+ } else {
+ // Unknown encoded entity...
+ sb.append("%").append(oldC).append(c);
+ percentSeen = false;
+ }
+ } else {
+ if (i == 1) {
+ sb.append(oldC);
+ }
+ sb.append(c);
+ }
+
+ if (percentSeen && (i == length - 1)) {
+ // At the end; incomplete entity found...
+ if (oldC != '%' && c != '%') {
+ sb.append("%").append(oldC).append(c);
+ } else if (oldC == '%' && c != '%') {
+ sb.append("%").append(c);
+ } else if (c == '%') {
+ sb.append("%");
+ }
+ }
+
+ oldC = c;
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDB.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDB.java
new file mode 100644
index 0000000..4eafab3
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDB.java
@@ -0,0 +1,155 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.mongodb;
+
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.mongodb.DB;
+import com.mongodb.DBCollection;
+import com.mongodb.Mongo;
+import com.mongodb.MongoException;
+import com.mongodb.ServerAddress;
+
+/**
+ * Provides a simple facade for accessing MongoDB.
+ */
+final class MongoDB {
+
+ private final List<ServerAddress> m_servers;
+ private final String m_dbName;
+ private final String m_collectionName;
+
+ private final AtomicReference<Mongo> m_mongoRef;
+
+ /**
+ * Creates a new {@link MongoDB} instance.
+ *
+ * @param serverNames the space separated list of Mongo servers, cannot be <code>null</code> or empty;
+ * @param dbName the name of the MongoDB to connect to, cannot be <code>null</code> or empty;
+ * @param collectionName the name of the collection to use, cannot be <code>null</code> or empty.
+ */
+ public MongoDB(String serverNames, String dbName, String collectionName) {
+ if (serverNames == null || "".equals(serverNames.trim())) {
+ throw new IllegalArgumentException("ServerNames cannot be null or empty!");
+ }
+ if (dbName == null || "".equals(dbName.trim())) {
+ throw new IllegalArgumentException("DbName cannot be null or empty!");
+ }
+ if (collectionName == null || "".equals(collectionName.trim())) {
+ throw new IllegalArgumentException("CollectionName cannot be null or empty!");
+ }
+
+ m_mongoRef = new AtomicReference<Mongo>();
+
+ m_servers = parseServers(serverNames);
+ m_dbName = dbName;
+ m_collectionName = collectionName;
+ }
+
+ /**
+ * Parses the space separated list of server names.
+ *
+ * @param serverNames the server names, cannot be <code>null</code>.
+ * @return a list of {@link ServerAddress}es to connect to, never <code>null</code>.
+ * @throws IllegalArgumentException in case the given server names was invalid.
+ */
+ private static List<ServerAddress> parseServers(String serverNames) {
+ String[] parts = serverNames.split("\\s+");
+
+ List<ServerAddress> servers = new ArrayList<ServerAddress>();
+ for (int i = 0; i < parts.length; i++) {
+ String part = parts[i];
+ try {
+ int colonPos = part.indexOf(":");
+ if (colonPos > 0 && (colonPos < part.length() - 1)) {
+ String name = part.substring(0, colonPos);
+ String portStr = part.substring(colonPos + 1);
+ servers.add(new ServerAddress(name, Integer.valueOf(portStr)));
+ }
+ }
+ catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Illegal port number in: " + part);
+ }
+ catch (UnknownHostException e) {
+ throw new IllegalArgumentException("Unknown host: " + part);
+ }
+ }
+
+ if (servers.isEmpty()) {
+ throw new IllegalArgumentException("No (valid) servers defined!");
+ }
+
+ return servers;
+ }
+
+ /**
+ * Connects to the MongoDB with the supplied credentials.
+ *
+ * @param userName the optional user name to use;
+ * @param password the optional password to use.
+ * @return <code>true</code> if the connection was succesful, <code>false</code> otherwise.
+ */
+ public boolean connect(String userName, String password) {
+ Mongo newMongo = new Mongo(m_servers);
+
+ Mongo oldMongo;
+ do {
+ oldMongo = m_mongoRef.get();
+ } while (!m_mongoRef.compareAndSet(oldMongo, newMongo));
+
+ DB db = newMongo.getDB(m_dbName);
+ if ((userName != null) && (password != null)) {
+ if (!db.authenticate(userName, password.toCharArray())) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the database collection to work in.
+ *
+ * @return the {@link DBCollection}, never <code>null</code>.
+ * @throws MongoException in case no connection to Mongo exists.
+ */
+ public DBCollection getCollection() {
+ Mongo mongo = m_mongoRef.get();
+ if (mongo == null) {
+ throw new MongoException("Not connected to MongoDB!");
+ }
+ DB db = mongo.getDB(m_dbName);
+ return db.getCollection(m_collectionName);
+ }
+
+ /**
+ * Disconnects from the MongoDB.
+ */
+ public void disconnect() {
+ Mongo mongo = m_mongoRef.get();
+ if (mongo != null) {
+ try {
+ mongo.close();
+ } finally {
+ m_mongoRef.compareAndSet(mongo, null);
+ }
+ }
+ }
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDBStore.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDBStore.java
new file mode 100644
index 0000000..18d722c
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDBStore.java
@@ -0,0 +1,429 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.mongodb;
+
+import static org.apache.felix.useradmin.mongodb.MongoSerializerHelper.NAME;
+import static org.apache.felix.useradmin.mongodb.MongoSerializerHelper.TYPE;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.log.LogService;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBCollection;
+import com.mongodb.DBCursor;
+import com.mongodb.DBObject;
+import com.mongodb.MongoException;
+import com.mongodb.WriteResult;
+
+/**
+ * Provides a repository store that uses MongoDB for storing the role information.
+ * <p>
+ * This service can also be configured at runtime by using the PID {@value #PID}.<br/>
+ * The configuration options recognized by this service are:
+ * </p>
+ * <dl>
+ * <dt>"useradmin.mongodb.server"</dt>
+ * <dd>A space separated string containing the MongoDB servers. The format for this string is: "<code><host1:port1> <host2:port2></code>". This value is mandatory;</dd>
+ * <dt>"useradmin.mongodb.name"</dt>
+ * <dd>A string containing the name of the database to use for this store. This value is mandatory;</dd>
+ * <dt>"useradmin.mongodb.collection"</dt>
+ * <dd>The name of the database collection to use for this store. This value is mandatory;</dd>
+ * <dt>"useradmin.mongodb.username"</dt>
+ * <dd>An optional string value representing the name of the user to authenticate against MongoDB;</dd>
+ * <dt>"useradmin.mongodb.password"</dt>
+ * <dd>An optional string value representing the password to authenticate against MongoDB.</dd>
+ * </dl>
+ * <p>
+ * Alternatively, one can also supply the above mentioned configuration keys as system properties. However,
+ * this implies that only a single store can be configured on a system!
+ * </p>
+ * <p>
+ * By default, the following values are used:
+ * </p>
+ * <table>
+ * <tr><td>"<tt>useradmin.mongodb.server</tt>"</td><td>"<tt>localhost:27017</tt>"</td></tr>
+ * <tr><td>"<tt>useradmin.mongodb.name</tt>"</td><td>"<tt>ua_repo</tt>"</td></tr>
+ * <tr><td>"<tt>useradmin.mongodb.collection</tt>"</td><td>"<tt>useradmin</tt>"</td></tr>
+ * <tr><td>"<tt>useradmin.mongodb.username</tt>"</td><td><none></td></tr>
+ * <tr><td>"<tt>useradmin.mongodb.password</tt>"</td><td><none></td></tr>
+ * </table>
+ * <p>
+ * This class is thread-safe.
+ * </p>
+ */
+public class MongoDBStore implements RoleProvider, RoleRepositoryStore, UserAdminListener, ManagedService {
+
+ /** The PID for the managed service reference. */
+ public static final String PID = "org.apache.felix.useradmin.mongodb";
+
+ /**
+ * A space-separated array with server definitions to access MongoDB.
+ * Format = "<host1:port1> <host2:port2>".
+ * */
+ private static final String KEY_MONGODB_SERVER = "useradmin.mongodb.server";
+ /** The name of the MongoDB database instance. */
+ private static final String KEY_MONGODB_NAME = "useradmin.mongodb.name";
+ /** The username of the MongoDB database instance. */
+ private static final String KEY_MONGODB_USERNAME = "useradmin.mongodb.username";
+ /** The password of the MongoDB database instance. */
+ private static final String KEY_MONGODB_PASSWORD = "useradmin.mongodb.password";
+ /** The name of the MongoDB collection to use. */
+ private static final String KEY_MONGODB_COLLECTION_NAME = "useradmin.mongodb.collection";
+
+ /** Default MongoDB server; first checks a system property */
+ private static final String DEFAULT_MONGODB_SERVER = System.getProperty(KEY_MONGODB_SERVER, "localhost:27017");
+ /** Default MongoDB name */
+ private static final String DEFAULT_MONGODB_NAME = System.getProperty(KEY_MONGODB_NAME, "ua_repo");
+ /** Default MongoDB collection */
+ private static final String DEFAULT_MONGODB_COLLECTION = System.getProperty(KEY_MONGODB_COLLECTION_NAME, "useradmin");
+ /** Default MongoDB username */
+ private static final String DEFAULT_MONGODB_USERNAME = System.getProperty(KEY_MONGODB_USERNAME);
+ /** Default MongoDB password */
+ private static final String DEFAULT_MONGODB_PASSWORD = System.getProperty(KEY_MONGODB_PASSWORD);
+
+ private final AtomicReference<MongoDB> m_mongoDbRef;
+ private final MongoSerializerHelper m_helper;
+
+ private volatile LogService m_log;
+
+ /**
+ * Creates a new {@link MongoDBStore} instance.
+ */
+ public MongoDBStore() {
+ m_mongoDbRef = new AtomicReference<MongoDB>();
+ m_helper = new MongoSerializerHelper(this);
+ }
+
+ @Override
+ public boolean addRole(Role role) throws IOException {
+ if (role == null) {
+ throw new IllegalArgumentException("Role cannot be null!");
+ }
+
+ try {
+ DBCollection coll = getCollection();
+
+ DBCursor cursor = coll.find(getTemplateObject(role));
+ try {
+ if (cursor.hasNext()) {
+ // Role already exists...
+ return false;
+ }
+ } finally {
+ cursor.close();
+ }
+
+ // Role does not exist; insert it...
+ DBObject data = m_helper.serialize(role);
+
+ WriteResult result = coll.insert(data);
+
+ if (result.getLastError() != null) {
+ result.getLastError().throwOnError();
+ }
+
+ return true;
+ }
+ catch (MongoException e) {
+ m_log.log(LogService.LOG_WARNING, "Add role failed!", e);
+ throw new IOException("AddRole failed!", e);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ MongoDB mongoDB = m_mongoDbRef.get();
+ if (mongoDB != null) {
+ mongoDB.disconnect();
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ // Nop
+ }
+
+ @Override
+ public Role[] getAllRoles() throws IOException {
+ try {
+ List<Role> roles = new ArrayList<Role>();
+
+ DBCollection coll = getCollection();
+
+ DBCursor cursor = coll.find();
+ try {
+ while (cursor.hasNext()) {
+ roles.add(m_helper.deserialize(cursor.next()));
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return roles.toArray(new Role[roles.size()]);
+ }
+ catch (MongoException e) {
+ m_log.log(LogService.LOG_WARNING, "Get all roles failed!", e);
+ throw new IOException("GetAllRoles failed!", e);
+ }
+ }
+
+ @Override
+ public Role getRole(String name) {
+ DBCollection coll = getCollection();
+
+ DBCursor cursor = coll.find(getTemplateObject(name));
+ try {
+ if (cursor.hasNext()) {
+ return m_helper.deserialize(cursor.next());
+ }
+ } finally {
+ cursor.close();
+ }
+
+ return null;
+ }
+
+ @Override
+ public Role getRoleByName(String name) throws IOException {
+ try {
+ return getRole(name);
+ }
+ catch (MongoException e) {
+ m_log.log(LogService.LOG_WARNING, "Get role by name failed!", e);
+ throw new IOException("GetRoleByName failed!", e);
+ }
+ }
+
+ @Override
+ public void initialize() throws IOException {
+ // Check whether we need to connect to MongoDB, or that this is
+ // already done by the #updated method...
+ MongoDB oldMongoDB = m_mongoDbRef.get();
+ if (oldMongoDB == null) {
+ MongoDB mongoDB = new MongoDB(DEFAULT_MONGODB_SERVER, DEFAULT_MONGODB_NAME, DEFAULT_MONGODB_COLLECTION);
+
+ do {
+ oldMongoDB = m_mongoDbRef.get();
+ }
+ while (!m_mongoDbRef.compareAndSet(oldMongoDB, mongoDB));
+
+ try {
+ connectToDB(mongoDB, DEFAULT_MONGODB_USERNAME, DEFAULT_MONGODB_PASSWORD);
+ }
+ catch (MongoException e) {
+ m_log.log(LogService.LOG_WARNING, "Initialization failed!", e);
+ throw new IOException("Initialization failed!", e);
+ }
+ }
+ }
+
+ @Override
+ public boolean removeRole(Role role) throws IOException {
+ try {
+ DBCollection coll = getCollection();
+
+ WriteResult result = coll.remove(getTemplateObject(role));
+
+ if (result.getLastError() != null) {
+ result.getLastError().throwOnError();
+ }
+
+ return true;
+ }
+ catch (MongoException e) {
+ m_log.log(LogService.LOG_WARNING, "Remove role failed!", e);
+ throw new IOException("RemoveRole failed!", e);
+ }
+ }
+
+ @Override
+ public void roleChanged(UserAdminEvent event) {
+ if (UserAdminEvent.ROLE_CHANGED == event.getType()) {
+ // Only the changes are interesting, as the creation and
+ // removal are already caught by #addRole and #removeRole....
+ Role changedRole = event.getRole();
+
+ try {
+ DBCollection coll = getCollection();
+
+ DBObject query = getTemplateObject(changedRole);
+ DBObject update = m_helper.serializeUpdate(changedRole);
+
+ WriteResult result = coll.update(query, update, false /* upsert */, false /* multi */);
+
+ if (result.getLastError() != null) {
+ result.getLastError().throwOnError();
+ }
+ }
+ catch (MongoException e) {
+ m_log.log(LogService.LOG_WARNING, "Failed to update changed role: " + changedRole.getName(), e);
+ }
+ }
+ }
+
+ /**
+ * @param log the log-service to set, cannot be <code>null</code>.
+ */
+ public void setLogService(LogService log) {
+ m_log = log;
+ }
+
+ @Override
+ public void updated(Dictionary properties) throws ConfigurationException {
+ String newServers = DEFAULT_MONGODB_SERVER;
+ String newDbName = DEFAULT_MONGODB_NAME;
+ String newCollectionName = DEFAULT_MONGODB_COLLECTION;
+ String newUsername = DEFAULT_MONGODB_USERNAME;
+ String newPassword = DEFAULT_MONGODB_PASSWORD;
+
+ if (properties != null) {
+ // Use values supplied...
+ newServers = getMandatoryProperty(properties, KEY_MONGODB_SERVER);
+ newDbName = getMandatoryProperty(properties, KEY_MONGODB_NAME);
+ newCollectionName = getMandatoryProperty(properties, KEY_MONGODB_COLLECTION_NAME);
+
+ newUsername = getProperty(properties, KEY_MONGODB_USERNAME);
+ newPassword = getProperty(properties, DEFAULT_MONGODB_PASSWORD);
+ }
+
+ MongoDB newMongoDb = new MongoDB(newServers, newDbName, newCollectionName);
+
+ MongoDB oldMongoDb;
+ do {
+ oldMongoDb = m_mongoDbRef.get();
+ }
+ while (!m_mongoDbRef.compareAndSet(oldMongoDb, newMongoDb));
+
+ try {
+ oldMongoDb.disconnect();
+ }
+ catch (MongoException e) {
+ m_log.log(LogService.LOG_WARNING, "Failed to disconnect from (old) MongoDB!", e);
+ }
+
+ try {
+ connectToDB(newMongoDb, newUsername, newPassword);
+ }
+ catch (MongoException e) {
+ m_log.log(LogService.LOG_WARNING, "Failed to connect to (new) MongoDB!", e);
+ throw new ConfigurationException(DEFAULT_MONGODB_USERNAME, "Failed to connect!", e);
+ }
+ }
+
+ /**
+ * Creates a connection to MongoDB using the given credentials.
+ *
+ * @param mongoDB the {@link MongoDB} facade to connect to;
+ * @param userName the (optional) user name to use;
+ * @param password the (optional) password to use.
+ * @throws MongoException in case the connection or authentication failed.
+ */
+ private void connectToDB(MongoDB mongoDB, String userName, String password) throws MongoException {
+ if (!mongoDB.connect(userName, password)) {
+ throw new MongoException("Failed to connect to MongoDB! Authentication failed!");
+ }
+
+ DBCollection collection = mongoDB.getCollection();
+ if (collection == null) {
+ throw new MongoException("Failed to connect to MongoDB! No collection returned!");
+ }
+
+ collection.ensureIndex(new BasicDBObject(NAME, 1).append("unique", true));
+ }
+
+ /**
+ * Returns the current database collection.
+ *
+ * @return the database collection to work with, cannot be <code>null</code>.
+ * @throws MongoException in case no connection to MongoDB exists.
+ */
+ private DBCollection getCollection() {
+ MongoDB mongoDB = m_mongoDbRef.get();
+ if (mongoDB == null) {
+ throw new MongoException("No connection to MongoDB?!");
+ }
+ return mongoDB.getCollection();
+ }
+
+ /**
+ * Returns the mandatory value for the given key.
+ *
+ * @param properties the properties to get the mandatory value from;
+ * @param key the key of the value to retrieve;
+ * @return the value, never <code>null</code>.
+ * @throws ConfigurationException in case the given key had no value.
+ */
+ private String getMandatoryProperty(Dictionary properties, String key) throws ConfigurationException {
+ String result = getProperty(properties, key);
+ if (result == null || "".equals(result.trim())) {
+ throw new ConfigurationException(key, "cannot be null or empty!");
+ }
+ return result;
+ }
+
+ /**
+ * Returns the value for the given key.
+ *
+ * @param properties the properties to get the value from;
+ * @param key the key of the value to retrieve;
+ * @return the value, can be <code>null</code> in case no such key is present.
+ * @throws ConfigurationException in case the given key had no value.
+ */
+ private String getProperty(Dictionary properties, String key) throws ConfigurationException {
+ Object result = properties.get(key);
+ if (result == null || !(result instanceof String)) {
+ return null;
+ }
+ return (String) result;
+ }
+
+ /**
+ * Creates a template object for the given role.
+ *
+ * @param role the role to create a template object for, cannot be <code>null</code>.
+ * @return a template object for MongoDB, never <code>null</code>.
+ */
+ private DBObject getTemplateObject(Role role) {
+ BasicDBObject query = new BasicDBObject();
+ query.put(TYPE, role.getType());
+ query.put(NAME, role.getName());
+ return query;
+ }
+
+ /**
+ * Creates a template object for the given (role)name.
+ *
+ * @param name the name of the role to create a template object for, cannot be <code>null</code>.
+ * @return a template object for MongoDB, never <code>null</code>.
+ */
+ private DBObject getTemplateObject(String name) {
+ BasicDBObject query = new BasicDBObject();
+ query.put(NAME, name);
+ return query;
+ }
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoSerializerHelper.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoSerializerHelper.java
new file mode 100644
index 0000000..31af118
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoSerializerHelper.java
@@ -0,0 +1,220 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.mongodb;
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.List;
+
+import org.apache.felix.useradmin.RoleFactory;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+import com.mongodb.BasicDBList;
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBObject;
+import com.mongodb.MongoException;
+
+/**
+ * Provides a helper class for (de)serializing data to/from MongoDB.
+ */
+final class MongoSerializerHelper {
+
+ static final String TYPE = "type";
+ static final String NAME = "name";
+
+ static final String PROPERTIES = "properties";
+ static final String CREDENTIALS = "credentials";
+ static final String MEMBERS = "members";
+ static final String REQUIRED_MEMBERS = "requiredMembers";
+
+ static final String SET = "$set";
+
+ private final RoleProvider m_roleProvider;
+
+ /**
+ * Creates a new {@link MongoSerializerHelper} instance.
+ *
+ * @param roleProvider the role provider to use, cannot be <code>null</code>.
+ */
+ public MongoSerializerHelper(RoleProvider roleProvider) {
+ m_roleProvider = roleProvider;
+ }
+
+ /**
+ * Converts a given {@link DBObject} to a {@link Role} instance.
+ *
+ * @param object the {@link DBObject} to convert, cannot be <code>null</code>.
+ * @return a {@link Role} instance, never <code>null</code>.
+ */
+ public Role deserialize(DBObject object) {
+ int type = ((Integer) object.get(TYPE)).intValue();
+ String name = (String) object.get(NAME);
+
+ Role result = RoleFactory.createRole(type, name);
+ // Read the generic properties of the role...
+ deserializeDictionary(result.getProperties(), (DBObject) object.get(PROPERTIES));
+
+ if ((Role.GROUP == type) || (Role.USER == type)) {
+ // This is safe, as Group extends from User...
+ deserializeDictionary(((User) result).getCredentials(), (DBObject) object.get(CREDENTIALS));
+
+ if (Role.GROUP == type) {
+ for (Role member : getRoles((BasicDBList) object.get(MEMBERS))) {
+ ((Group) result).addMember(member);
+ }
+
+ for (Role member : getRoles((BasicDBList) object.get(REQUIRED_MEMBERS))) {
+ ((Group) result).addRequiredMember(member);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Serializes the given {@link Role} to a {@link DBObject} instance.
+ *
+ * @param role the {@link Role} to serialize, cannot be <code>null</code> (unchecked!).
+ * @return a {@link DBObject} representing the given {@link Role}, never <code>null</code>.
+ */
+ public DBObject serialize(Role role) {
+ BasicDBObject data = new BasicDBObject();
+
+ int type = role.getType();
+
+ data.put(TYPE, type);
+ data.put(NAME, role.getName());
+
+ data.put(PROPERTIES, serializeDictionary(role.getProperties()));
+ if ((Role.GROUP == type) || (Role.USER == type)) {
+ data.put(CREDENTIALS, serializeDictionary(((User) role).getCredentials()));
+
+ if (Role.GROUP == type) {
+ data.put(MEMBERS, getRoleNames(((Group) role).getMembers()));
+ data.put(REQUIRED_MEMBERS, getRoleNames(((Group) role).getRequiredMembers()));
+ }
+ }
+
+ return data;
+ }
+
+ /**
+ * Creates a serialized version of the given {@link Role} to be used in an update statement.
+ *
+ * @param role the {@link Role} to update, cannot be <code>null</code>.
+ * @return a {@link DBObject} representing an update statement for the given {@link Role}.
+ */
+ public DBObject serializeUpdate(Role role) {
+ int type = role.getType();
+
+ BasicDBObject changeSet = new BasicDBObject();
+
+ changeSet.put(PROPERTIES, serializeDictionary(role.getProperties()));
+ if ((Role.GROUP == type) || (Role.USER == type)) {
+ changeSet.put(CREDENTIALS, serializeDictionary(((User) role).getCredentials()));
+
+ if (Role.GROUP == type) {
+ changeSet.put(MEMBERS, getRoleNames(((Group) role).getMembers()));
+ changeSet.put(REQUIRED_MEMBERS, getRoleNames(((Group) role).getRequiredMembers()));
+ }
+ }
+
+ return new BasicDBObject(SET, changeSet);
+ }
+
+ /**
+ * Finds an existing member by its name.
+ *
+ * @param name the name of the member to return, cannot be <code>null</code>.
+ * @return a member instance, never <code>null</code>.
+ * @throws MongoException in case the requested member was not found (or any other MongoDB exception).
+ */
+ final Role findExistingMember(String name) {
+ Role result = m_roleProvider.getRole(name);
+ if (result == null) {
+ throw new MongoException("No such role: " + name);
+ }
+ return result;
+ }
+
+ /**
+ * Deserializes the given {@link DBObject} into the given {@link Dictionary}.
+ *
+ * @param dictionary the dictionary to fill;
+ * @param object the {@link DBObject} to deserialize.
+ */
+ private void deserializeDictionary(Dictionary dictionary, DBObject object) {
+ for (String key : object.keySet()) {
+ dictionary.put(KeyCodec.decode(key), object.get(key));
+ }
+ }
+
+ /**
+ * Serializes a given array of {@link Role}s to an list for storing in a {@link DBObject}.
+ *
+ * @param members the {@link Role}s to serialize, cannot be <code>null</code>.
+ * @return the "serialized" array, never <code>null</code>.
+ */
+ private List<String> getRoleNames(Role[] members) {
+ List<String> result = new ArrayList<String>();
+ if (members != null) {
+ for (Role member : members) {
+ result.add(member.getName());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns all roles mentioned in the given list.
+ *
+ * @param list the list with role names to convert.
+ * @return a list with {@link Role}s, never <code>null</code>.
+ */
+ private List<Role> getRoles(BasicDBList list) {
+ List<Role> result = new ArrayList<Role>();
+ for (int i = 0, size = list.size(); i < size; i++) {
+ final String memberName = (String) list.get(i);
+ result.add(findExistingMember(memberName));
+ }
+ return result;
+ }
+
+ /**
+ * Serializes a given {@link Dictionary} into a {@link DBObject}.
+ *
+ * @param properties the {@link Dictionary} to serialize, cannot be <code>null</code>.
+ * @return the serialized dictionary, never <code>null</code>.
+ */
+ private DBObject serializeDictionary(Dictionary properties) {
+ BasicDBObject result = new BasicDBObject();
+
+ Enumeration<String> keysEnum = properties.keys();
+ while (keysEnum.hasMoreElements()) {
+ String key = keysEnum.nextElement();
+ Object value = properties.get(key);
+
+ result.append(KeyCodec.encode(key), value);
+ }
+
+ return result;
+ }
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/RoleProvider.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/RoleProvider.java
new file mode 100644
index 0000000..82f8b23
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/RoleProvider.java
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.mongodb;
+
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Provides access to a {@link Role}.
+ */
+public interface RoleProvider {
+
+ /**
+ * Returns the {@link Role} with the given name.
+ *
+ * @param name the name of the role to return, cannot be <code>null</code>.
+ * @return a {@link Role} with the given name, or <code>null</code> if no such role exists.
+ */
+ Role getRole(String name);
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/Activator.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/Activator.java
new file mode 100644
index 0000000..632af63
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/Activator.java
@@ -0,0 +1,60 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.mongodb.osgi;
+
+import java.util.Properties;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.apache.felix.useradmin.mongodb.MongoDBStore;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Registers {@link MongoDBStore} as service.
+ */
+public class Activator implements BundleActivator {
+
+ private LogServiceHelper m_logServiceHelper;
+
+ @Override
+ public void start(BundleContext context) throws Exception {
+ m_logServiceHelper = new LogServiceHelper(context);
+ m_logServiceHelper.open();
+
+ MongoDBStore store = new MongoDBStore();
+ store.setLogService(m_logServiceHelper);
+
+ Properties props = new Properties();
+ props.put(Constants.SERVICE_PID, MongoDBStore.PID);
+
+ String[] serviceNames = { RoleRepositoryStore.class.getName(),
+ UserAdminListener.class.getName(), ManagedService.class.getName() };
+
+ context.registerService(serviceNames, store, props);
+ }
+
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ if (m_logServiceHelper != null) {
+ m_logServiceHelper.close();
+ m_logServiceHelper = null;
+ }
+ }
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/LogServiceHelper.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/LogServiceHelper.java
new file mode 100644
index 0000000..5397cf9
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/LogServiceHelper.java
@@ -0,0 +1,77 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.mongodb.osgi;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogService;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Provides a facade for {@link LogService} allowing it to be not
+ * available as actual service without its users knowing about this.
+ */
+public class LogServiceHelper extends ServiceTracker implements LogService {
+
+ /**
+ * Creates a new {@link LogServiceHelper} instance.
+ *
+ * @param context the bundle context to use, cannot be <code>null</code>.
+ */
+ public LogServiceHelper(BundleContext context) {
+ super(context, LogService.class.getName(), null);
+ }
+
+ @Override
+ public void log(int level, String message) {
+ LogService logService = getLogService();
+ if (logService != null) {
+ logService.log(level, message);
+ }
+ }
+
+ @Override
+ public void log(int level, String message, Throwable exception) {
+ LogService logService = getLogService();
+ if (logService != null) {
+ logService.log(level, message, exception);
+ }
+ }
+
+ @Override
+ public void log(ServiceReference sr, int level, String message) {
+ LogService logService = getLogService();
+ if (logService != null) {
+ logService.log(sr, level, message);
+ }
+ }
+
+ @Override
+ public void log(ServiceReference sr, int level, String message, Throwable exception) {
+ LogService logService = getLogService();
+ if (logService != null) {
+ logService.log(sr, level, message, exception);
+ }
+ }
+
+ /**
+ * @return a {@link LogService} instance, or <code>null</code> if not available.
+ */
+ private LogService getLogService() {
+ return (LogService) getService();
+ }
+}
diff --git a/useradmin/mongodb/src/test/java/org/apache/felix/useradmin/mongodb/KeyCodecTest.java b/useradmin/mongodb/src/test/java/org/apache/felix/useradmin/mongodb/KeyCodecTest.java
new file mode 100644
index 0000000..ca9ba58
--- /dev/null
+++ b/useradmin/mongodb/src/test/java/org/apache/felix/useradmin/mongodb/KeyCodecTest.java
@@ -0,0 +1,202 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.mongodb;
+
+import junit.framework.TestCase;
+
+/**
+ * Test cases for {@link KeyCodec}.
+ */
+public class KeyCodecTest extends TestCase {
+
+ /**
+ * Tests that we can decode a string with an encoded dollar.
+ */
+ public void testDecodeDollarOk() {
+ String input = "%24hello %24world";
+ assertEquals("$hello $world", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that we can decode a string with encoded dot and dollars.
+ */
+ public void testDecodeDotAndDollarsOk() {
+ String input = "%24hello %24world%2E";
+ assertEquals("$hello $world.", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that we can decode a string with an encoded dot.
+ */
+ public void testDecodeDotOk() {
+ String input = "hello world%2E";
+ assertEquals("hello world.", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that encoding a string with a dot works.
+ */
+ public void testDecodeEncodeOk() {
+ String key = "%25hello world.";
+ assertEquals(key, KeyCodec.decode(KeyCodec.encode(key)));
+ }
+
+ /**
+ * Tests that we can decode a string with an incorrect entity at the end.
+ */
+ public void testDecodeIncorrectEntityAtEndOk() {
+ String input = "hello world%1";
+ assertEquals("hello world%1", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that we can decode a string with an incorrect entity at the end.
+ */
+ public void testDecodeIncorrectEntityOk() {
+ String input = "hello%1world";
+ assertEquals("hello%1world", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that we can decode a string with an encoded null.
+ */
+ public void testDecodeNullEntityOk() {
+ String input = "%00hello%00world";
+ assertEquals("\0hello\0world", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that we can decode a null value.
+ */
+ public void testDecodeNullOk() {
+ assertNull(KeyCodec.decode(null));
+ }
+
+ /**
+ * Tests that we can decode a string with a percent sign at the end.
+ */
+ public void testDecodePercentAtEndOk() {
+ String input = "hello world%";
+ assertEquals("hello world%", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that we can decode a string with an escaped percent.
+ */
+ public void testDecodePercentOk() {
+ String input = "%%1%%%2E";
+ assertEquals("%1%.", KeyCodec.decode(input));
+
+ input = "%%Hello %%World%2E";
+ assertEquals("%Hello %World.", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that we can decode a string with an encoded underscore.
+ */
+ public void testDecodeUnderscoreOk() {
+ String input = "%5Fhello%5Fworld";
+ assertEquals("_hello_world", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that we can decode a string with an unknown entity at the end.
+ */
+ public void testDecodeUnknownEntityAtEndOk() {
+ String input = "hello world%10";
+ assertEquals("hello world%10", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that we can decode a string with an unknown entity.
+ */
+ public void testDecodeUnknownEntityOk() {
+ String input = "%25hello %25world%2F";
+ assertEquals("%25hello %25world%2F", KeyCodec.decode(input));
+ }
+
+ /**
+ * Tests that encoding a string with dollars works.
+ */
+ public void testEncodeDollarOk() {
+ String key = "$hello $world";
+ assertEquals("%24hello %24world", KeyCodec.encode(key));
+ }
+
+ /**
+ * Tests that encoding a string with dollars and a dot works.
+ */
+ public void testEncodeDotAndDollarOk() {
+ String key = "$hello $world.";
+ assertEquals("%24hello %24world%2E", KeyCodec.encode(key));
+ }
+
+ /**
+ * Tests that encoding a string with a dot works.
+ */
+ public void testEncodeDotOk() {
+ String key = "hello world.";
+ assertEquals("hello world%2E", KeyCodec.encode(key));
+ }
+
+ /**
+ * Tests that encoding a string with null-characters works.
+ */
+ public void testEncodeNullEntityOk() {
+ String key = "\0hello\0world";
+ assertEquals("%00hello%00world", KeyCodec.encode(key));
+ }
+
+ /**
+ * Tests that we can encode a null value.
+ */
+ public void testEncodeNullInputOk() {
+ assertNull(KeyCodec.encode(null));
+ }
+
+ /**
+ * Tests that encoding a string with percents works.
+ */
+ public void testEncodePercentOk() {
+ String key = "%hello %world.";
+ assertEquals("%%hello %%world%2E", KeyCodec.encode(key));
+ }
+
+ /**
+ * Tests that encoding a string without dots or dollars works.
+ */
+ public void testEncodePlainStringOk() {
+ String key = "hello world!";
+ assertEquals(key, KeyCodec.encode(key));
+ }
+
+ /**
+ * Tests that encoding a string with underscores works.
+ */
+ public void testEncodeUnderscoreOk() {
+ String key = "_hello_world";
+ assertEquals("%5Fhello%5Fworld", KeyCodec.encode(key));
+ }
+
+ /**
+ * Tests that encoding a string with a dot works.
+ */
+ public void testEncodeUnknownEntitiesOk() {
+ String key = "%25hello world.";
+ assertEquals("%%25hello world%2E", KeyCodec.encode(key));
+ }
+}
diff --git a/useradmin/pom.xml b/useradmin/pom.xml
new file mode 100644
index 0000000..7ddf619
--- /dev/null
+++ b/useradmin/pom.xml
@@ -0,0 +1,36 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>felix-parent</artifactId>
+ <version>1.2.0</version>
+ <relativePath>../../pom/pom.xml</relativePath>
+ </parent>
+ <packaging>pom</packaging>
+ <artifactId>useradmin-reactor</artifactId>
+ <description>UserAdmin OSGi compendium service subproject.</description>
+ <version>1</version>
+ <modules>
+ <module>useradmin</module>
+ <module>filestore</module>
+ <module>mongodb</module>
+ </modules>
+</project>
diff --git a/useradmin/useradmin/pom.xml b/useradmin/useradmin/pom.xml
new file mode 100644
index 0000000..daaae11
--- /dev/null
+++ b/useradmin/useradmin/pom.xml
@@ -0,0 +1,100 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <artifactId>felix-parent</artifactId>
+ <groupId>org.apache.felix</groupId>
+ <version>1.2.0</version>
+ <relativePath>../../../pom/pom.xml</relativePath>
+ </parent>
+ <artifactId>org.apache.felix.useradmin</artifactId>
+ <version>1.0.2</version>
+ <packaging>bundle</packaging>
+ <description>Provides an implementation for the UserAdmin OSGi compendium service.</description>
+ <dependencies>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <version>4.0.0</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ <version>4.0.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.framework</artifactId>
+ <version>4.0.2</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Bundle-Name>Apache Felix User Admin Service</Bundle-Name>
+ <Bundle-Description>
+ An implementation of the OSGi User Admin Compendium Service
+ </Bundle-Description>
+ <Bundle-Activator>
+ ${project.artifactId}.osgi.Activator
+ </Bundle-Activator>
+ <Bundle-SymbolicName>
+ ${project.artifactId}
+ </Bundle-SymbolicName>
+ <Bundle-Vendor>The Apache Software Foundation</Bundle-Vendor>
+ <Import-Package>
+ org.osgi.service.useradmin; version="[1.1,1.2)",
+ *
+ </Import-Package>
+ <Export-Package>
+ ${project.artifactId}; version="1.0.0"
+ </Export-Package>
+ <Private-Package>
+ ${project.artifactId}.*
+ </Private-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>rat-maven-plugin</artifactId>
+ <configuration>
+ <excludeSubProjects>false</excludeSubProjects>
+ <useEclipseDefaultExcludes>true</useEclipseDefaultExcludes>
+ <useMavenDefaultExcludes>true</useMavenDefaultExcludes>
+ <excludes>
+ <param>doc/*</param>
+ <param>maven-eclipse.xml</param>
+ <param>.checkstyle</param>
+ <param>.externalToolBuilders/*</param>
+ </excludes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/BackendException.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/BackendException.java
new file mode 100644
index 0000000..be24d47
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/BackendException.java
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin;
+
+/**
+ * Represents a runtime exception for {@link RoleRepositoryBackend}s.
+ */
+public class BackendException extends RuntimeException {
+
+ private static final long serialVersionUID = 5566633157189079557L;
+
+ /**
+ * Creates a new {@link BackendException} instance.
+ *
+ * @param cause the originating cause of this exception.
+ */
+ public BackendException(Exception cause) {
+ super(cause);
+ }
+
+ /**
+ * Creates a new {@link BackendException} instance.
+ *
+ * @param message the message of this exception.
+ */
+ public BackendException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new {@link BackendException} instance.
+ *
+ * @param message the message of this exception;
+ * @param cause the originating cause of this exception.
+ */
+ public BackendException(String message, Exception cause) {
+ super(message, cause);
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleFactory.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleFactory.java
new file mode 100644
index 0000000..da54c9e
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleFactory.java
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin;
+
+
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Provides a factory for creating the various role instances, can be used by external
+ * implementations to create new role instances.
+ */
+public final class RoleFactory {
+
+ /**
+ * Creates a new instance of {@link RoleFactory}, not used.
+ */
+ private RoleFactory() {
+ // Nop
+ }
+
+ /**
+ * Creates a new role instance.
+ *
+ * @param type the type of the role to create;
+ * @param name the name of the role to create.
+ * @return a new {@link RoleImpl} instance denoting the requested role, never <code>null</code>.
+ */
+ public static Role createRole(int type, String name) {
+ if (type == Role.USER) {
+ UserImpl result = new UserImpl(name);
+ return result;
+ } else if (type == Role.GROUP) {
+ GroupImpl result = new GroupImpl(name);
+ return result;
+ } else {
+ RoleImpl result = new RoleImpl(name);
+ return result;
+ }
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleRepositoryStore.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleRepositoryStore.java
new file mode 100644
index 0000000..3296f59
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleRepositoryStore.java
@@ -0,0 +1,94 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Provides an abstraction to store and retrieve a role repository.
+ */
+public interface RoleRepositoryStore extends Closeable {
+
+ /**
+ * Adds a given role to this backend.
+ * <p>
+ * If the given role is already contained by this backed, this method
+ * should not do anything and return <code>false</code> to denote this.
+ * </p>
+ *
+ * @param role the role to add, cannot be <code>null</code>.
+ * @return <code>true</code> if the role was successfully added, <code>false</code> otherwise.
+ * @throws IllegalArgumentException in case the given argument was <code>null</code>;
+ * @throws IOException in case of I/O problems.
+ */
+ boolean addRole(Role role) throws IOException;
+
+ /**
+ * Closes this store, allowing implementations to free up resources, close
+ * connections, and so on.
+ *
+ * @throws IOException in case of I/O problems.
+ */
+ void close() throws IOException;
+
+ /**
+ * Returns all available roles in this backend.
+ *
+ * @return an array with all roles, never <code>null</code>, but can be empty.
+ * @throws IOException in case of I/O problems.
+ */
+ Role[] getAllRoles() throws IOException;
+
+ /**
+ * Returns a {@link Role} by its name.
+ *
+ * @param roleName the name of the role to return, cannot be <code>null</code>.
+ * @return the role with the given name, or <code>null</code> if no such role exists.
+ * @throws IllegalArgumentException in case the given argument was <code>null</code>;
+ * @throws IOException in case of I/O problems.
+ */
+ Role getRoleByName(String roleName) throws IOException;
+
+ /**
+ * Called once before any other method of this interface is being called.
+ * <p>
+ * Implementations can use this method to create a connection to the
+ * backend, or load the initial set of roles, and so on.
+ * </p>
+ *
+ * @throws IOException in case of I/O problems.
+ */
+ void initialize() throws IOException;
+
+ /**
+ * Removes a given role from this backend.
+ * <p>
+ * If the given role is not contained by this backed, this method
+ * should not do anything and return <code>false</code> to denote this.
+ * </p>
+ *
+ * @param role the role to remove, cannot be <code>null</code>.
+ * @return <code>true</code> if the role was successfully removed, <code>false</code> otherwise.
+ * @throws IllegalArgumentException in case the given argument was <code>null</code>;
+ * @throws IOException in case of I/O problems.
+ */
+ boolean removeRole(Role role) throws IOException;
+
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/AuthorizationImpl.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/AuthorizationImpl.java
new file mode 100644
index 0000000..2234ebc
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/AuthorizationImpl.java
@@ -0,0 +1,97 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.impl;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Provides an implementation for {@link Authorization}.
+ */
+public class AuthorizationImpl implements Authorization {
+
+ private final String m_name;
+ private final User m_user;
+ private final RoleRepository m_roleManager;
+ private final RoleChecker m_roleChecker;
+
+ /**
+ * Creates a new {@link AuthorizationImpl} instance for the given {@link User}.
+ *
+ * @param roleManager the role manager to use for obtaining the roles, cannot be <code>null</code>.
+ */
+ public AuthorizationImpl(RoleRepository roleManager) {
+ this(null, roleManager);
+ }
+
+ /**
+ * Creates a new {@link AuthorizationImpl} instance for the given {@link User}.
+ *
+ * @param user the {@link User} to authorize, may be <code>null</code> for the anonymous user;
+ * @param roleManager the role manager to use for obtaining the roles, cannot be <code>null</code>.
+ */
+ public AuthorizationImpl(User user, RoleRepository roleManager) {
+ m_user = user;
+ m_roleManager = roleManager;
+ m_name = (user != null) ? user.getName() : null;
+ m_roleChecker = new RoleChecker();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getName() {
+ return m_name;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean hasRole(String name) {
+ Role role = m_roleManager.getRoleByName(name);
+ if (role == null) {
+ // No role found, so it is never implied...
+ return false;
+ }
+ return m_roleChecker.isImpliedBy(role, m_user);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String[] getRoles() {
+ List result = new ArrayList();
+
+ Iterator rolesIter = m_roleManager.getRoles(null /* filter */).iterator();
+ while (rolesIter.hasNext()) {
+ RoleImpl role = (RoleImpl) rolesIter.next();
+ if (!Role.USER_ANYONE.equals(role.getName()) && m_roleChecker.isImpliedBy(role, m_user)) {
+ result.add(role.getName());
+ }
+ }
+
+ return result.isEmpty() ? null : (String[]) result.toArray(new String[result.size()]);
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/EventDispatcher.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/EventDispatcher.java
new file mode 100644
index 0000000..d769bee
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/EventDispatcher.java
@@ -0,0 +1,226 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl;
+
+import java.util.Properties;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.event.EventConstants;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Provides an event dispatcher for delivering {@link UserAdminEvent}s asynchronously.
+ */
+public final class EventDispatcher implements Runnable {
+
+ private static final String TOPIC_BASE = "org/osgi/service/useradmin/UserAdmin/";
+
+ private final EventAdmin m_eventAdmin;
+ private final UserAdminListenerList m_listenerList;
+ private final BlockingQueue m_eventQueue;
+ private final Thread m_backgroundThread;
+
+ /**
+ * Creates a new {@link EventDispatcher} instance, and starts a background thread to deliver all events.
+ *
+ * @param eventAdmin the event admin to use, cannot be <code>null</code>;
+ * @param listenerList the list with {@link UserAdminListener}s, cannot be <code>null</code>.
+ * @throws IllegalArgumentException in case one of the given parameters was <code>null</code>.
+ */
+ public EventDispatcher(EventAdmin eventAdmin, UserAdminListenerList listenerList) {
+ if (eventAdmin == null) {
+ throw new IllegalArgumentException("EventAdmin cannot be null!");
+ }
+ if (listenerList == null) {
+ throw new IllegalArgumentException("ListenerList cannot be null!");
+ }
+
+ m_eventAdmin = eventAdmin;
+ m_listenerList = listenerList;
+ m_eventQueue = new LinkedBlockingQueue();
+
+ m_backgroundThread = new Thread(this, "UserAdmin event dispatcher");
+ }
+
+ /**
+ * Dispatches a given event for asynchronous delivery to all interested listeners,
+ * including those using the {@link EventAdmin} service.
+ * <p>
+ * This method will perform a best-effort to dispatch the event to all listeners, i.e.,
+ * there is no guarantee that the listeners will actually obtain the event, nor any
+ * notification is given in case delivery fails.
+ * </p>
+ *
+ * @param event the event to dispatch, cannot be <code>null</code>.
+ * @throws IllegalStateException in case this dispatcher is already stopped.
+ */
+ public void dispatch(UserAdminEvent event) {
+ if (!isRunning()) {
+ return;
+ }
+
+ try {
+ m_eventQueue.put(event);
+ } catch (InterruptedException e) {
+ // Restore interrupt flag...
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Starts this event dispatcher, allowing it to pick up events and deliver them.
+ */
+ public void start() {
+ if (!isRunning()) {
+ m_backgroundThread.start();
+ }
+ }
+
+ /**
+ * Signals this event dispatcher to stop its work and clean up all running threads.
+ */
+ public void stop() {
+ if (!isRunning()) {
+ return;
+ }
+
+ // Add poison object to queue to let the background thread terminate...
+ m_eventQueue.add(EventDispatcher.this);
+
+ try {
+ m_backgroundThread.join();
+ } catch (InterruptedException e) {
+ // We're already stopping; so don't bother...
+ }
+ }
+
+ /**
+ * Returns whether or not the background thread is running.
+ *
+ * @return <code>true</code> if the background thread is running (alive), <code>false</code> otherwise.
+ */
+ final boolean isRunning() {
+ return m_backgroundThread.isAlive();
+ }
+
+ /**
+ * Provides the main event loop, which waits until an event is enqueued in order
+ * to deliver it to any interested listener.
+ */
+ public void run() {
+ try {
+ while (true) {
+ // Blocks until a event is dispatched...
+ Object event = m_eventQueue.take();
+
+ if (event instanceof UserAdminEvent) {
+ // Got a "normal" user admin event; lets dispatch it further...
+ deliverEventSynchronously((UserAdminEvent) event);
+ } else {
+ // Got a "poison" object; this means we must stop running...
+ return;
+ }
+ }
+ } catch (InterruptedException e) {
+ // Restore interrupt flag, and terminate thread...
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Converts a given {@link UserAdminEvent} to a {@link Event} that can be
+ * dispatched through the {@link EventAdmin} service.
+ *
+ * @param event
+ * the event to convert, cannot be <code>null</code>.
+ * @return a new {@link Event} instance containing the same set of
+ * information as the given event, never <code>null</code>.
+ */
+ private Event convertEvent(UserAdminEvent event) {
+ String topic = getTopicName(event.getType());
+ Role role = event.getRole();
+ ServiceReference serviceRef = event.getServiceReference();
+
+ Properties props = new Properties();
+ props.put(EventConstants.EVENT_TOPIC, TOPIC_BASE.concat(topic));
+ props.put(EventConstants.EVENT, event);
+ props.put("role", role);
+ props.put("role.name", role.getName());
+ props.put("role.type", new Integer(role.getType()));
+ if (serviceRef != null) {
+ props.put(EventConstants.SERVICE, serviceRef);
+ Object property;
+
+ property = serviceRef.getProperty(Constants.SERVICE_ID);
+ if (property != null) {
+ props.put(EventConstants.SERVICE_ID, property);
+ }
+ property = serviceRef.getProperty(Constants.OBJECTCLASS);
+ if (property != null) {
+ props.put(EventConstants.SERVICE_OBJECTCLASS, property);
+ }
+ property = serviceRef.getProperty(Constants.SERVICE_PID);
+ if (property != null) {
+ props.put(EventConstants.SERVICE_PID, property);
+ }
+ }
+
+ return new Event(topic, props);
+ }
+
+ /**
+ * Delivers the given event synchronously to all interested listeners.
+ *
+ * @param event the event to deliver, cannot be <code>null</code>.
+ */
+ private void deliverEventSynchronously(UserAdminEvent event) {
+ // Asynchronously deliver an event to the EventAdmin service...
+ m_eventAdmin.postEvent(convertEvent(event));
+
+ // Synchronously call all UserAdminListeners to deliver the event...
+ UserAdminListener[] listeners = m_listenerList.getListeners();
+ for (int i = 0; i < listeners.length; i++) {
+ listeners[i].roleChanged(event);
+ }
+ }
+
+ /**
+ * Converts a topic name for the given event-type.
+ *
+ * @param type the type of event to get the topic name for.
+ * @return a topic name, never <code>null</code>.
+ */
+ private String getTopicName(int type) {
+ switch (type) {
+ case UserAdminEvent.ROLE_CREATED:
+ return "ROLE_CREATED";
+ case UserAdminEvent.ROLE_CHANGED:
+ return "ROLE_CHANGED";
+ case UserAdminEvent.ROLE_REMOVED:
+ return "ROLE_REMOVED";
+ default:
+ return null;
+ }
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChangeListener.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChangeListener.java
new file mode 100644
index 0000000..f9a914b
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChangeListener.java
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl;
+
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Provides a callback for listening to role changes.
+ */
+public interface RoleChangeListener {
+
+ /**
+ * Called when a new role is added.
+ *
+ * @param role the role that is added.
+ */
+ void roleAdded(Role role);
+
+ /**
+ * Called when a role is removed.
+ *
+ * @param role the role that is removed.
+ */
+ void roleRemoved(Role role);
+
+ /**
+ * Called when a new property-entry is added to a role.
+ *
+ * @param role the role that changed;
+ * @param key the key of the entry;
+ * @param value the value associated to the key.
+ */
+ void propertyAdded(Role role, Object key, Object value);
+
+ /**
+ * Called when an property-entry is removed from a role.
+ *
+ * @param role the role that changed;
+ * @param key the key of the entry.
+ */
+ void propertyRemoved(Role role, Object key);
+
+ /**
+ * Called when an property-entry is changed for a role.
+ *
+ * @param role the role that changed;
+ * @param key the key of the entry;
+ * @param oldValue the old value associated to the key;
+ * @param newValue the new value associated to the key.
+ */
+ void propertyChanged(Role role, Object key, Object oldValue, Object newValue);
+}
\ No newline at end of file
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChecker.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChecker.java
new file mode 100644
index 0000000..ced47ba
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChecker.java
@@ -0,0 +1,132 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Helper class to check for implied role memberships.
+ */
+final class RoleChecker {
+
+ /**
+ * Verifies whether the given role is implied by the memberships of the given user.
+ *
+ * @param user the user to check the roles for, cannot be <code>null</code>;
+ * @param impliedRole the implied role to check for, cannot be <code>null</code>.
+ * @return <code>true</code> if the given user has the implied role, <code>false</code> otherwise.
+ */
+ public boolean isImpliedBy(Role role, Role impliedRole) {
+ if (role instanceof Group) {
+ return isGroupImpliedBy((Group) role, impliedRole, new ArrayList());
+ } else if (role instanceof User) {
+ return isUserImpliedBy((User) role, impliedRole);
+ } else {
+ return isRoleImpliedBy(role, impliedRole);
+ }
+ }
+
+ /**
+ * Verifies whether the given group is implied by the given role.
+ *
+ * @param group the group to check, cannot be <code>null</code>;
+ * @param impliedRole the implied role to check for, cannot be <code>null</code>;
+ * @param seenGroups a list of all seen groups, used for detecting cycles in groups, cannot be <code>null</code>.
+ * @return <code>true</code> if the given group has the implied role, <code>false</code> otherwise.
+ */
+ private boolean isGroupImpliedBy(Group group, Role impliedRole, List seenGroups) {
+ Role[] basicRoles = group.getMembers();
+ Role[] requiredRoles = group.getRequiredMembers();
+
+ boolean isImplied = true;
+
+ // Check whether all required roles are implied...
+ for (int i = 0; (requiredRoles != null) && isImplied && (i < requiredRoles.length); i++) {
+ Role requiredRole = requiredRoles[i];
+ if (seenGroups.contains(requiredRole)) {
+ // Found a cycle between groups; always yield false!
+ return false;
+ }
+
+ if (requiredRole instanceof Group) {
+ seenGroups.add(requiredRole);
+ isImplied = isGroupImpliedBy((Group) requiredRole, impliedRole, seenGroups);
+ } else if (requiredRole instanceof User) {
+ isImplied = isUserImpliedBy((User) requiredRole, impliedRole);
+ } else /* if (requiredRoles[i] instanceof RoleImpl) */ {
+ isImplied = isRoleImpliedBy(requiredRole, impliedRole);
+ }
+ }
+
+ // Required role is not implied by the given role; we can stop now...
+ if (!isImplied) {
+ return false;
+ }
+
+ // Ok; all required roles are implied, let's verify whether a least one basic role is implied...
+ isImplied = false;
+
+ // Check whether at least one basic role is implied...
+ for (int i = 0; (basicRoles != null) && !isImplied && (i < basicRoles.length); i++) {
+ Role basicRole = (Role) basicRoles[i];
+ if (seenGroups.contains(basicRole)) {
+ // Found a cycle between groups; always yield false!
+ return false;
+ }
+
+ if (basicRole instanceof Group) {
+ seenGroups.add(basicRole);
+ isImplied = isGroupImpliedBy((Group) basicRole, impliedRole, seenGroups);
+ } else if (basicRole instanceof UserImpl) {
+ isImplied = isUserImpliedBy((User) basicRole, impliedRole);
+ } else /* if (requiredRoles[i] instanceof RoleImpl) */ {
+ isImplied = isRoleImpliedBy(basicRole, impliedRole);
+ }
+ }
+
+ return isImplied;
+ }
+
+ /**
+ * Verifies whether the given user is implied by the given role.
+ *
+ * @param user the user to check, cannot be <code>null</code>;
+ * @param impliedRole the implied role to check for, cannot be <code>null</code>;
+ * @return <code>true</code> if the given user is implied by the given role, <code>false</code> otherwise.
+ */
+ private boolean isUserImpliedBy(User user, Role impliedRole) {
+ return Role.USER_ANYONE.equals(user.getName()) || (impliedRole != null && impliedRole.getName().equals(user.getName()));
+ }
+
+ /**
+ * Verifies whether the given role is implied by the given role.
+ *
+ * @param role the role to check, cannot be <code>null</code>;
+ * @param impliedRole the implied role to check for, cannot be <code>null</code>;
+ * @return <code>true</code> if the given role is implied by the given role, <code>false</code> otherwise.
+ */
+ private boolean isRoleImpliedBy(Role role, Role impliedRole) {
+ return Role.USER_ANYONE.equals(role.getName()) || (impliedRole != null && impliedRole.getName().equals(role.getName()));
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleRepository.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleRepository.java
new file mode 100644
index 0000000..2348b55
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleRepository.java
@@ -0,0 +1,367 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.felix.useradmin.BackendException;
+import org.apache.felix.useradmin.RoleFactory;
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.osgi.framework.Filter;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminPermission;
+
+/**
+ * Provides a manager and entry-point for accessing {@link Role}s.
+ */
+public final class RoleRepository {
+
+ /**
+ * Hands off all obtained role change event to a local set of listeners.
+ */
+ final class RoleChangeReflector implements RoleChangeListener {
+ /**
+ * {@inheritDoc}
+ */
+ public void roleAdded(Role role) {
+ Iterator iterator = createListenerIterator();
+ while (iterator.hasNext()) {
+ ((RoleChangeListener) iterator.next()).roleAdded(role);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void roleRemoved(Role role) {
+ Iterator iterator = createListenerIterator();
+ while (iterator.hasNext()) {
+ ((RoleChangeListener) iterator.next()).roleRemoved(role);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void propertyAdded(Role role, Object key, Object value) {
+ Iterator iterator = createListenerIterator();
+ while (iterator.hasNext()) {
+ ((RoleChangeListener) iterator.next()).propertyAdded(role, key, value);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void propertyRemoved(Role role, Object key) {
+ Iterator iterator = createListenerIterator();
+ while (iterator.hasNext()) {
+ ((RoleChangeListener) iterator.next()).propertyRemoved(role, key);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void propertyChanged(Role role, Object key, Object oldValue, Object newValue) {
+ Iterator iterator = createListenerIterator();
+ while (iterator.hasNext()) {
+ ((RoleChangeListener) iterator.next()).propertyChanged(role, key, oldValue, newValue);
+ }
+ }
+ }
+
+ /** The single predefined role. */
+ public static final Role USER_ANYONE = RoleFactory.createRole(Role.ROLE, Role.USER_ANYONE);
+
+ private final RoleRepositoryStore m_store;
+ private final CopyOnWriteArrayList m_listeners;
+ private final RoleChangeReflector m_roleChangeReflector;
+
+ /**
+ * Creates a new {@link RoleRepository} instance.
+ *
+ * @param store the {@link RoleRepositoryStore} to use, cannot be <code>null</code>.
+ */
+ public RoleRepository(RoleRepositoryStore store) {
+ m_store = store;
+
+ m_listeners = new CopyOnWriteArrayList();
+ m_roleChangeReflector = new RoleChangeReflector();
+ }
+
+ /**
+ * Adds a given role to this manager.
+ *
+ * @param role the role to add, cannot be <code>null</code>. If it is already contained by this manager, this method will not do anything.
+ * @return the given role if added, <code>null</code> otherwise.
+ */
+ public Role addRole(Role role) {
+ if (role == null) {
+ throw new IllegalArgumentException("Role cannot be null!");
+ }
+ if (!(role instanceof RoleImpl)) {
+ throw new IllegalArgumentException("Invalid role type!");
+ }
+
+ checkPermissions();
+
+ try {
+ if (m_store.addRole(role)) {
+ m_roleChangeReflector.roleAdded(role);
+ return wireChangeListener(role);
+ }
+
+ return null;
+ }
+ catch (IOException e) {
+ throw new BackendException("Adding role " + role.getName() + " failed!", e);
+ }
+ }
+
+ /**
+ * Adds the given role change listener to be called for upcoming changes in roles.
+ *
+ * @param listener the listener to register, cannot be <code>null</code>.
+ * @throws IllegalArgumentException in case the given listener was <code>null</code>.
+ */
+ public void addRoleChangeListener(RoleChangeListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("RoleChangeListener cannot be null!");
+ }
+
+ m_listeners.addIfAbsent(listener);
+ }
+
+ /**
+ * Returns the by its given name.
+ *
+ * @param roleName the name of the role to return, cannot be <code>null</code>.
+ * @return the role matching the given name, or <code>null</code> if no role matched the given name.
+ */
+ public Role getRoleByName(String roleName) {
+ try {
+ return wireChangeListener(m_store.getRoleByName(roleName));
+ }
+ catch (IOException e) {
+ throw new BackendException("Failed to get role by name: " + roleName + "!", e);
+ }
+ }
+
+ /**
+ * Returns a collection with all roles matching a given filter.
+ *
+ * @param filter the filter to match the individual roles against, can be <code>null</code> if all roles should be returned.
+ * @return a list with all matching roles, can be empty, but never <code>null</code>.
+ */
+ public List getRoles(Filter filter) {
+ List matchingRoles = new ArrayList();
+
+ try {
+ Role[] roles = m_store.getAllRoles();
+ for (int i = 0; i < roles.length; i++) {
+ Role role = roles[i];
+ if (!isPredefinedRole(role) && ((filter == null) || filter.match(role.getProperties()))) {
+ matchingRoles.add(wireChangeListener(role));
+ }
+ }
+ }
+ catch (IOException e) {
+ throw new BackendException("Failed to get roles!", e);
+ }
+
+ return matchingRoles;
+ }
+
+ /**
+ * Returns a collection with all roles matching a given key-value pair.
+ *
+ * @param key the key to search for;
+ * @param value the value to search for.
+ * @return a list with all matching roles, can be empty, but never <code>null</code>.
+ */
+ public List getRoles(String key, String value) {
+ if (key == null) {
+ throw new IllegalArgumentException("Key cannot be null!");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("Value cannot be null!");
+ }
+
+ List matchingRoles = new ArrayList();
+
+ try {
+ Role[] roles = m_store.getAllRoles();
+ for (int i = 0; i < roles.length; i++) {
+ Role role = roles[i];
+ Dictionary dict = role.getProperties();
+ if (!isPredefinedRole(role) && value.equals(dict.get(key))) {
+ matchingRoles.add(wireChangeListener(role));
+ }
+ }
+ }
+ catch (IOException e) {
+ throw new BackendException("Failed to get roles!", e);
+ }
+
+ return matchingRoles;
+ }
+
+ /**
+ * Removes a given role from this manager.
+ *
+ * @param role the role to remove, cannot be <code>null</code>.
+ * @return <code>true</code> if the role was removed (i.e., it was managed by this manager), or <code>false</code> if it was not found.
+ */
+ public boolean removeRole(Role role) {
+ if (role == null) {
+ throw new IllegalArgumentException("Role cannot be null!");
+ }
+ if (!(role instanceof RoleImpl)) {
+ throw new IllegalArgumentException("Invalid role type!");
+ }
+
+ checkPermissions();
+
+ // Cannot remove predefined roles...
+ if (isPredefinedRole(role)) {
+ return false;
+ }
+
+ try {
+ if (m_store.removeRole(role)) {
+ unwireChangeListener(role);
+ m_roleChangeReflector.roleRemoved(role);
+
+ return true;
+ }
+
+ return false;
+ }
+ catch (IOException e) {
+ throw new BackendException("Failed to remove role " + role.getName() + "!", e);
+ }
+ }
+
+ /**
+ * Removes the given role change listener from be called for changes in roles.
+ *
+ * @param listener the listener to unregister, cannot be <code>null</code>.
+ * @throws IllegalArgumentException in case the given listener was <code>null</code>.
+ */
+ public void removeRoleChangeListener(RoleChangeListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("RoleChangeListener cannot be null!");
+ }
+
+ m_listeners.remove(listener);
+ }
+
+ /**
+ * Starts this repository.
+ */
+ public void start() {
+ try {
+ // The sole predefined role we've got...
+ m_store.addRole(USER_ANYONE);
+
+ m_store.initialize();
+ }
+ catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Stops this repository, allowing it to clean up.
+ */
+ public void stop() {
+ try {
+ m_store.close();
+ }
+ catch (IOException e) {
+ // Ignore; nothing we can do about this here...
+ }
+ }
+
+ /**
+ * Creates a new iterator for iterating over all listeners.
+ *
+ * @return a new {@link Iterator} instance, never <code>null</code>.
+ */
+ final Iterator createListenerIterator() {
+ return m_listeners.iterator();
+ }
+
+ /**
+ * Verifies whether the caller has the right permissions to add or remove roles.
+ *
+ * @throws SecurityException in case the caller has not the right permissions to perform the action.
+ */
+ private void checkPermissions() throws SecurityException {
+ SecurityManager securityManager = System.getSecurityManager();
+ if (securityManager != null) {
+ securityManager.checkPermission(new UserAdminPermission(UserAdminPermission.ADMIN, null));
+ }
+ }
+
+ /**
+ * Returns whether or not the given role is a predefined role.
+ * <p>
+ * Currently, there's only a single predefined role: {@link Role#USER_ANYONE}.
+ * </p>
+ *
+ * @param role the role to check, may be <code>null</code>.
+ * @return <code>true</code> if the given role is predefined, <code>false</code> otherwise.
+ */
+ private boolean isPredefinedRole(Role role) {
+ return Role.USER_ANYONE.equals(role.getName());
+ }
+
+ /**
+ * Wires the given role to this repository so it can listen for its changes.
+ *
+ * @param role the role to listen for its changes, cannot be <code>null</code>.
+ * @return the given role.
+ * @throws IllegalArgumentException in case the given object was not a {@link RoleImpl} instance.
+ */
+ private Role wireChangeListener(Object role) {
+ RoleImpl result = (RoleImpl) role;
+ if (result != null) {
+ result.setRoleChangeListener(m_roleChangeReflector);
+ }
+ return result;
+ }
+
+ /**
+ * Unwires the given role to this repository so it no longer listens for its changes.
+ *
+ * @param role the role to unwire, cannot be <code>null</code>.
+ * @throws IllegalArgumentException in case the given object was not a {@link RoleImpl} instance.
+ */
+ private void unwireChangeListener(Object role) {
+ RoleImpl result = (RoleImpl) role;
+ result.setRoleChangeListener(null);
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminImpl.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminImpl.java
new file mode 100644
index 0000000..9e51c52
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminImpl.java
@@ -0,0 +1,210 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.impl;
+
+import java.util.List;
+
+import org.apache.felix.useradmin.RoleFactory;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceFactory;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/**
+ * Provides the implementation for {@link UserAdmin}.
+ */
+public class UserAdminImpl implements ServiceFactory, UserAdmin, RoleChangeListener {
+
+ private final RoleRepository m_roleRepository;
+ private final EventDispatcher m_eventDispatcher;
+
+ private volatile ServiceReference m_serviceRef;
+
+ /**
+ * Creates a new {@link UserAdminImpl} implementation.
+ *
+ * @param roleRepository the repository with roles to use for this service;
+ * @param eventDispatcher the event dispatcher to use for this service.
+ *
+ * @throws IllegalArgumentException in case one of the given parameters was <code>null</code>.
+ */
+ public UserAdminImpl(RoleRepository roleRepository, EventDispatcher eventDispatcher) {
+ if (roleRepository == null) {
+ throw new IllegalArgumentException("RoleRepository cannot be null!");
+ }
+ if (eventDispatcher == null) {
+ throw new IllegalArgumentException("EventDispatcher cannot be null!");
+ }
+
+ m_roleRepository = roleRepository;
+ m_eventDispatcher = eventDispatcher;
+
+ m_roleRepository.addRoleChangeListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Role createRole(String name, int type) {
+ if ((type != Role.USER) && (type != Role.GROUP)) {
+ throw new IllegalArgumentException("Invalid type, must by either Role.USER or Role.GROUP!");
+ }
+ if (name == null) {
+ throw new IllegalArgumentException("Invalid name, must be non-null and non-empty!");
+ }
+
+ return m_roleRepository.addRole(RoleFactory.createRole(type, name));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Authorization getAuthorization(User user) {
+ return new AuthorizationImpl(user, m_roleRepository);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Role getRole(String name) {
+ return m_roleRepository.getRoleByName(name);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Role[] getRoles(String filter) throws InvalidSyntaxException {
+ List roles = m_roleRepository.getRoles(createFilter(filter));
+ if (roles.isEmpty()) {
+ return null;
+ }
+ return (Role[]) roles.toArray(new Role[roles.size()]);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Overridden in order to get hold of our service reference.</p>
+ */
+ public Object getService(Bundle bundle, ServiceRegistration registration) {
+ m_serviceRef = registration.getReference();
+ return this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public User getUser(String key, String value) {
+ User result = null;
+ List roles = m_roleRepository.getRoles(key, value);
+ if (roles.size() == 1) {
+ Role foundRole = (Role) roles.get(0);
+ if (foundRole.getType() == Role.USER) {
+ result = (User) foundRole;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void propertyAdded(Role role, Object key, Object value) {
+ m_eventDispatcher.dispatch(createUserAdminEvent(UserAdminEvent.ROLE_CHANGED, role));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void propertyChanged(Role role, Object key, Object oldValue, Object newValue) {
+ m_eventDispatcher.dispatch(createUserAdminEvent(UserAdminEvent.ROLE_CHANGED, role));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void propertyRemoved(Role role, Object key) {
+ m_eventDispatcher.dispatch(createUserAdminEvent(UserAdminEvent.ROLE_CHANGED, role));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean removeRole(String name) {
+ Role role = getRole(name);
+ if (role == null) {
+ return false;
+ }
+
+ return m_roleRepository.removeRole(role);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void roleAdded(Role role) {
+ m_eventDispatcher.dispatch(createUserAdminEvent(UserAdminEvent.ROLE_CREATED, role));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void roleRemoved(Role role) {
+ m_eventDispatcher.dispatch(createUserAdminEvent(UserAdminEvent.ROLE_REMOVED, role));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) {
+ // Nop; we leave the service as-is...
+ }
+
+ /**
+ * Creates a {@link Filter} instance for the given OSGi/LDAP filter.
+ *
+ * @param filter the filter to convert to a {@link Filter} instance.
+ * @return a {@link Filter} instance corresponding to the given filter string, never <code>null</code>.
+ * @throws InvalidSyntaxException in case the given filter was invalid.
+ */
+ protected Filter createFilter(String filter) throws InvalidSyntaxException {
+ if (filter == null || "".equals(filter.trim())) {
+ return null;
+ }
+ return FrameworkUtil.createFilter(filter);
+ }
+
+ /**
+ * Creates a new {@link UserAdminEvent} instance for the given type and role.
+ *
+ * @param type the type of event to create;
+ * @param role the role to create the event for.
+ * @return a new {@link UserAdminEvent} instance, never <code>null</code>.
+ */
+ private UserAdminEvent createUserAdminEvent(int type, Role role) {
+ return new UserAdminEvent(m_serviceRef, type, role);
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminListenerList.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminListenerList.java
new file mode 100644
index 0000000..9afa6a0
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminListenerList.java
@@ -0,0 +1,33 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl;
+
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Provides an abstraction for a list of {@link UserAdminListener}s.
+ */
+public interface UserAdminListenerList {
+
+ /**
+ * Returns all current listeners.
+ *
+ * @return an array of {@link UserAdminListener}s, never <code>null</code>,
+ * but can be an empty array.
+ */
+ UserAdminListener[] getListeners();
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/GroupImpl.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/GroupImpl.java
new file mode 100644
index 0000000..6786a95
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/GroupImpl.java
@@ -0,0 +1,181 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl.role;
+
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminPermission;
+
+/**
+ * Provides an implementation of {@link Group}.
+ */
+public class GroupImpl extends UserImpl implements Group {
+
+ private static final String BASIC_MEMBER = "basicMember";
+ private static final String REQUIRED_MEMBER = "requiredMember";
+
+ private final Object m_lock = new Object();
+
+ private final Map m_members;
+ private final Map m_requiredMembers;
+
+ /**
+ * Creates a new {@link GroupImpl} instance of type {@link Role#GROUP}.
+ *
+ * @param name the name of this group role, cannot be <code>null</code> or empty.
+ */
+ public GroupImpl(String name) {
+ super(Role.GROUP, name);
+
+ m_members = new HashMap();
+ m_requiredMembers = new HashMap();
+ }
+
+ /**
+ * Creates a new {@link GroupImpl} instance of type {@link Role#GROUP}.
+ *
+ * @param name the name of this group role, cannot be <code>null</code> or empty.
+ */
+ public GroupImpl(String name, Dictionary properties, Dictionary credentials) {
+ super(Role.GROUP, name, properties, credentials);
+
+ m_members = new HashMap();
+ m_requiredMembers = new HashMap();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean addMember(Role role) {
+ checkPermissions();
+
+ Object result;
+ synchronized (m_lock) {
+ if (m_members.containsKey(role.getName()) || m_requiredMembers.containsKey(role.getName())) {
+ return false;
+ }
+ result = m_members.put(role.getName(), role);
+ }
+
+ if (result == null) {
+ // Notify our (optional) listener...
+ entryAdded(BASIC_MEMBER, role);
+ }
+
+ return (result == null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean addRequiredMember(Role role) {
+ checkPermissions();
+
+ Object result;
+ synchronized (m_lock) {
+ if (m_requiredMembers.containsKey(role.getName()) || m_members.containsKey(role.getName())) {
+ return false;
+ }
+ result = m_requiredMembers.put(role.getName(), role);
+ }
+
+ if (result == null) {
+ // Notify our (optional) listener...
+ entryAdded(REQUIRED_MEMBER, role);
+ }
+
+ return (result == null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Role[] getMembers() {
+ Role[] roles;
+ synchronized (m_lock) {
+ Collection values = m_members.values();
+ roles = (Role[]) values.toArray(new Role[values.size()]);
+ }
+
+ return (roles.length == 0) ? null : roles;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Role[] getRequiredMembers() {
+ Role[] roles;
+ synchronized (m_lock) {
+ Collection values = m_requiredMembers.values();
+ roles = (Role[]) values.toArray(new Role[values.size()]);
+ }
+
+ return (roles.length == 0) ? null : roles;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean removeMember(Role role) {
+ checkPermissions();
+
+ String key = null;
+ Object result = null;
+
+ synchronized (m_lock) {
+ if (m_requiredMembers.containsKey(role.getName())) {
+ key = REQUIRED_MEMBER;
+ result = m_requiredMembers.remove(role.getName());
+ }
+ else if (m_members.containsKey(role.getName())) {
+ key = BASIC_MEMBER;
+ result = m_members.remove(role.getName());
+ }
+ }
+
+ if (result != null) {
+ // Notify our (optional) listener...
+ entryRemoved(key);
+ }
+
+ return result != null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String toString() {
+ return "Group(" + getName() + "): R{" + m_requiredMembers + "}, B{" + m_members + "}";
+ }
+
+ /**
+ * Verifies whether the caller has the right permissions to get or change the given key.
+ *
+ * @throws SecurityException in case the caller has not the right permissions to perform the action.
+ */
+ private void checkPermissions() throws SecurityException {
+ SecurityManager securityManager = System.getSecurityManager();
+ if (securityManager != null) {
+ securityManager.checkPermission(new UserAdminPermission(UserAdminPermission.ADMIN, null));
+ }
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableDictionary.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableDictionary.java
new file mode 100644
index 0000000..a41ae14
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableDictionary.java
@@ -0,0 +1,311 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl.role;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.osgi.service.useradmin.UserAdminPermission;
+
+/**
+ * Provides an observable {@link Dictionary} implementation that emits change
+ * events for the put and remove operations aside checking for security
+ * permissions for all accessor methods.
+ */
+class ObservableDictionary extends Dictionary implements Serializable {
+
+ private static final long serialVersionUID = 9223154895541178975L;
+
+ /**
+ * Provides a listener for changes to a {@link ObservableDictionary}.
+ */
+ static interface DictionaryChangeListener {
+
+ /**
+ * Called when a new entry is added.
+ *
+ * @param key the key of the entry;
+ * @param value the value associated to the key.
+ */
+ void entryAdded(Object key, Object value);
+
+ /**
+ * Called when an entry is changed.
+ *
+ * @param key the key of the entry;
+ * @param oldValue the old value associated to the key;
+ * @param newValue the new value associated to the key.
+ */
+ void entryChanged(Object key, Object oldValue, Object newValue);
+
+ /**
+ * Called when an entry is removed.
+ *
+ * @param key the key of the entry.
+ */
+ void entryRemoved(Object key);
+ }
+
+ /**
+ * Provides a wrapper to convert an {@link Iterator} to an {@link Enumeration} implementation.
+ */
+ static final class IteratorEnumeration implements Enumeration {
+
+ private final Iterator m_iterator;
+
+ /**
+ * Creates a new {@link IteratorEnumeration}.
+ *
+ * @param iterator the {@link Iterator} to convert to a {@link Enumeration}, cannot be <code>null</code>.
+ */
+ public IteratorEnumeration(Iterator iterator) {
+ m_iterator = iterator;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean hasMoreElements() {
+ return m_iterator.hasNext();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Object nextElement() {
+ return m_iterator.next();
+ }
+ }
+
+ /**
+ * Converts a given {@link Dictionary} implementation to a {@link Map} implementation.
+ *
+ * @param dictionary the dictionary to convert, cannot be <code>null</code>.
+ * @return a {@link Map} instance with all the same key-value pairs as the given dictionary, never <code>null</code>.
+ */
+ private static ConcurrentMap convertToMap(Dictionary dictionary) {
+ ConcurrentMap result = new ConcurrentHashMap();
+ if (dictionary instanceof Map) {
+ result.putAll((Map) dictionary);
+ } else {
+ Enumeration keyEnum = dictionary.keys();
+ while (keyEnum.hasMoreElements()) {
+ Object key = keyEnum.nextElement();
+ result.put(key, dictionary.get(key));
+ }
+ }
+ return result;
+ }
+
+ private final ConcurrentMap m_properties;
+ private final String m_getAction;
+ private final String m_changeAction;
+
+ private transient volatile DictionaryChangeListener m_listener;
+
+ /**
+ * Creates a new, empty, {@link ObservableDictionary} instance.
+ */
+ public ObservableDictionary(String getAction, String changeAction) {
+ m_getAction = getAction;
+ m_changeAction = changeAction;
+ m_properties = new ConcurrentHashMap();
+ }
+
+ /**
+ * Creates a new {@link ObservableDictionary} instance with the given dictionary as defaults.
+ *
+ * @param dictionary the defaults to set for this properties, cannot be <code>null</code>.
+ */
+ public ObservableDictionary(String getAction, String changeAction, Dictionary dictionary) {
+ if (dictionary == null) {
+ throw new IllegalArgumentException("Dictionary cannot be null!");
+ }
+ m_getAction = getAction;
+ m_changeAction = changeAction;
+ m_properties = convertToMap(dictionary);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Enumeration elements() {
+ Collection values = m_properties.values();
+ return new IteratorEnumeration(values.iterator());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || (getClass() != object.getClass())) {
+ return false;
+ }
+
+ ObservableDictionary other = (ObservableDictionary) object;
+ if (m_properties == null) {
+ if (other.m_properties != null) {
+ return false;
+ }
+ } else if (!m_properties.equals(other.m_properties)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Object get(Object key) {
+ if (key == null) {
+ throw new IllegalArgumentException("Key cannot be null!");
+ }
+
+ if (m_getAction != null) {
+ checkPermissions(getAsPermissionKey(key), m_getAction);
+ }
+
+ return m_properties.get(key);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int hashCode() {
+ final int prime = 37;
+ int result = 1;
+ result = prime * result + ((m_properties == null) ? 0 : m_properties.hashCode());
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isEmpty() {
+ return m_properties.isEmpty();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Enumeration keys() {
+ Collection keys = m_properties.keySet();
+ return new IteratorEnumeration(keys.iterator());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Object put(Object key, Object value) {
+ if (key == null) {
+ throw new IllegalArgumentException("Key cannot be null!");
+ }
+ if (value == null) {
+ throw new IllegalArgumentException("Value cannot be null!");
+ }
+
+ if (m_changeAction != null) {
+ checkPermissions(getAsPermissionKey(key), m_changeAction);
+ }
+
+ Object oldValue = m_properties.put(key, value);
+
+ final DictionaryChangeListener listener = m_listener;
+ if (listener != null) {
+ if (oldValue == null) {
+ listener.entryAdded(key, value);
+ } else {
+ listener.entryChanged(key, oldValue, value);
+ }
+ }
+
+ return oldValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Object remove(Object key) {
+ if (key == null) {
+ throw new IllegalArgumentException("Key cannot be null!");
+ }
+
+ if (m_changeAction != null) {
+ checkPermissions(getAsPermissionKey(key), m_changeAction);
+ }
+
+ Object oldValue = m_properties.remove(key);
+ final DictionaryChangeListener listener = m_listener;
+ if (listener != null) {
+ listener.entryRemoved(key);
+ }
+
+ return oldValue;
+ }
+
+ /**
+ * Sets a new {@link DictionaryChangeListener} to observe changes to this dictionary.
+ *
+ * @param listener the listener to add, can be <code>null</code> to stop listening for changes.
+ */
+ public void setDictionaryChangeListener(DictionaryChangeListener listener) {
+ m_listener = listener;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int size() {
+ return m_properties.size();
+ }
+
+ /**
+ * @param key
+ * @return
+ */
+ protected String getAsPermissionKey(Object key) {
+ String k = UserAdminPermission.ADMIN;
+ if (key instanceof String) {
+ k = (String) key;
+ }
+ return k;
+ }
+
+ /**
+ * Verifies whether the caller has the right permissions to get or change the given key.
+ *
+ * @param key the name of the property that is to be accessed or changed, cannot be <code>null</code>;
+ * @param action the action name to perform, cannot be <code>null</code>.
+ * @throws SecurityException in case the caller has not the right permissions to perform the action.
+ */
+ private void checkPermissions(String key, String action) throws SecurityException {
+ SecurityManager securityManager = System.getSecurityManager();
+ if (securityManager != null) {
+ securityManager.checkPermission(new UserAdminPermission(key, action));
+ }
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableProperties.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableProperties.java
new file mode 100644
index 0000000..de05dc9
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableProperties.java
@@ -0,0 +1,80 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl.role;
+
+import java.util.Dictionary;
+
+/**
+ * Provides an stricter variant of the {@link ObservableDictionary} that only
+ * permits string keys and values of either String or byte[].
+ */
+final class ObservableProperties extends ObservableDictionary {
+
+ private static final long serialVersionUID = -2513082903921734796L;
+
+ /**
+ * Creates a new, empty, {@link ObservableProperties} instance.
+ */
+ public ObservableProperties(String getAction, String changeAction) {
+ super(getAction, changeAction);
+ }
+
+ /**
+ * Creates a new {@link ObservableProperties} instance with the given dictionary as defaults.
+ *
+ * @param dictionary the defaults to set for this properties, cannot be <code>null</code>.
+ */
+ public ObservableProperties(String getAction, String changeAction, Dictionary dictionary) {
+ super(getAction, changeAction, dictionary);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Object get(Object key) {
+ if (!(key instanceof String)) {
+ throw new IllegalArgumentException("Key must be of type String!");
+ }
+
+ return super.get(key);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Object put(Object key, Object value) {
+ if (!(key instanceof String)) {
+ throw new IllegalArgumentException("Key must be of type String!");
+ }
+ if (!(value instanceof String) && !(value instanceof byte[])) {
+ throw new IllegalArgumentException("Value must be of type String or byte[]!");
+ }
+
+ return super.put(key, value);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Object remove(Object key) {
+ if (!(key instanceof String)) {
+ throw new IllegalArgumentException("Key must be of type String!");
+ }
+
+ return super.remove(key);
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/RoleImpl.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/RoleImpl.java
new file mode 100644
index 0000000..2541968
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/RoleImpl.java
@@ -0,0 +1,185 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl.role;
+
+import java.util.Dictionary;
+
+
+import org.apache.felix.useradmin.impl.RoleChangeListener;
+import org.apache.felix.useradmin.impl.role.ObservableDictionary.DictionaryChangeListener;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminPermission;
+
+/**
+ * Provides an implementation of {@link Role}.
+ */
+public class RoleImpl implements Role, DictionaryChangeListener {
+
+ private final ObservableProperties m_properties;
+ private final String m_name;
+ private final int m_type;
+
+ private volatile RoleChangeListener m_listener;
+
+ /**
+ * Creates a new {@link RoleImpl} instance of type {@link Role#ROLE} and a given name.
+ *
+ * @param name the name of this role, cannot be <code>null</code> or empty.
+ */
+ public RoleImpl(String name) {
+ this(Role.ROLE, name);
+ }
+
+ /**
+ * Creates a new {@link RoleImpl} instance with a given type and name.
+ *
+ * @param type the type of this role, should be {@link Role#ROLE}, {@link Role#USER} or {@link Role#GROUP};
+ * @param name the name of this role, cannot be <code>null</code> or empty.
+ */
+ protected RoleImpl(int type, String name) {
+ if (name == null || "".equals(name.trim())) {
+ throw new IllegalArgumentException("Name cannot be null or empty!");
+ }
+ m_type = type;
+ m_name = name;
+ m_properties = new ObservableProperties(null, UserAdminPermission.CHANGE_PROPERTY);
+ m_properties.setDictionaryChangeListener(this);
+ }
+
+ /**
+ * Creates a new {@link RoleImpl} instance with a given type, name and properties.
+ *
+ * @param type the type of this role, should be {@link Role#ROLE}, {@link Role#USER} or {@link Role#GROUP};
+ * @param name the name of this role, cannot be <code>null</code> or empty;
+ * @param properties the initial properties of this role, cannot be <code>null</code>.
+ */
+ protected RoleImpl(int type, String name, Dictionary properties) {
+ if (name == null || "".equals(name.trim())) {
+ throw new IllegalArgumentException("Name cannot be null or empty!");
+ }
+ m_type = type;
+ m_name = name;
+ m_properties = new ObservableProperties(null, UserAdminPermission.CHANGE_PROPERTY, properties);
+ m_properties.setDictionaryChangeListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public final void entryAdded(Object key, Object value) {
+ RoleChangeListener listener = m_listener;
+ if (listener != null) {
+ listener.propertyAdded(this, key, value);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public final void entryRemoved(Object key) {
+ RoleChangeListener listener = m_listener;
+ if (listener != null) {
+ listener.propertyRemoved(this, key);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public final void entryChanged(Object key, Object oldValue, Object newValue) {
+ RoleChangeListener listener = m_listener;
+ if (listener != null) {
+ listener.propertyChanged(this, key, oldValue, newValue);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if ((obj == null) || (getClass() != obj.getClass())) {
+ return false;
+ }
+
+ RoleImpl other = (RoleImpl) obj;
+ if (m_name == null) {
+ if (other.m_name != null) {
+ return false;
+ }
+ } else if (!m_name.equals(other.m_name)) {
+ return false;
+ }
+
+ if (m_type != other.m_type) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String getName() {
+ return m_name;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Dictionary getProperties() {
+ return m_properties;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int getType() {
+ return m_type;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((m_name == null) ? 0 : m_name.hashCode());
+ result = prime * result + m_type;
+ return result;
+ }
+
+ /**
+ * Sets the {@link RoleChangeListener} for this role implementation.
+ *
+ * @param listener the listener to set, may be <code>null</code> to stop listening.
+ */
+ public void setRoleChangeListener(RoleChangeListener listener) {
+ m_listener = listener;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String toString() {
+ return "Role(" + getName() + ")";
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/UserImpl.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/UserImpl.java
new file mode 100644
index 0000000..d8cffc4
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/UserImpl.java
@@ -0,0 +1,119 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl.role;
+
+import java.util.Arrays;
+import java.util.Dictionary;
+
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminPermission;
+
+/**
+ * Provides an implementation of {@link User}.
+ */
+public class UserImpl extends RoleImpl implements User {
+
+ private final ObservableProperties m_credentials;
+
+ /**
+ * Creates a new {@link UserImpl} instance with type {@link Role#USER}.
+ *
+ * @param name the name of this user role, cannot be <code>null</code> or empty.
+ */
+ public UserImpl(String name) {
+ this(Role.USER, name);
+ }
+
+ /**
+ * Creates a new {@link UserImpl} instance with a given type.
+ *
+ * @param type the type of this role;
+ * @param name the name of this role, cannot be <code>null</code> or empty.
+ */
+ protected UserImpl(int type, String name) {
+ super(type, name);
+
+ m_credentials = new ObservableProperties(UserAdminPermission.GET_CREDENTIAL, UserAdminPermission.CHANGE_CREDENTIAL);
+ m_credentials.setDictionaryChangeListener(this);
+ }
+
+ /**
+ * Creates a new {@link UserImpl} instance with type {@link Role#USER}.
+ *
+ * @param name the name of this user role, cannot be <code>null</code> or empty.
+ */
+ protected UserImpl(int type, String name, Dictionary properties, Dictionary credentials) {
+ super(type, name, properties);
+
+ m_credentials = new ObservableProperties(UserAdminPermission.GET_CREDENTIAL, UserAdminPermission.CHANGE_CREDENTIAL, credentials);
+ m_credentials.setDictionaryChangeListener(this);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Dictionary getCredentials() {
+ return m_credentials;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean hasCredential(String key, Object value) {
+ // Will throw a SecurityException if we're not allowed to do this!
+ Object result = m_credentials.get(key);
+
+ // Be a bit more lenient with the various results we can get...
+ if (result instanceof String) {
+ String s1 = (String) result;
+ String s2;
+ if (value instanceof byte[]) {
+ s2 = new String((byte[]) value);
+ } else if (value instanceof String) {
+ s2 = (String) value;
+ } else {
+ // Not a string or a byte-array!
+ return false;
+ }
+
+ return s1.equals(s2);
+ } else if (result instanceof byte[]) {
+ byte[] b1 = (byte[]) result;
+ byte[] b2;
+ if (value instanceof byte[]) {
+ b2 = (byte[]) value;
+ } else if (value instanceof String) {
+ b2 = ((String) value).getBytes();
+ } else {
+ // Not a string or a byte-array!
+ return false;
+ }
+
+ return Arrays.equals(b1, b2);
+ }
+
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String toString() {
+ return "User(" + getName() + ")";
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/Activator.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/Activator.java
new file mode 100644
index 0000000..1b99b89
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/Activator.java
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.osgi;
+
+import org.apache.felix.useradmin.impl.EventDispatcher;
+import org.apache.felix.useradmin.impl.RoleRepository;
+import org.apache.felix.useradmin.impl.UserAdminImpl;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.useradmin.UserAdmin;
+
+/**
+ * Provides the bundle activator for the UserAdmin service.
+ */
+public class Activator implements BundleActivator {
+
+ private volatile ServiceContext m_context;
+
+ /**
+ * {@inheritDoc}
+ */
+ public void start(BundleContext context) throws Exception {
+ m_context = createServiceContext(context);
+
+ // The actual service itself...
+ UserAdminImpl service = new UserAdminImpl(m_context.m_roleRepository, m_context.m_eventDispatcher);
+
+ // Register the actual service...
+ context.registerService(UserAdmin.class.getName(), service, null);
+
+ // Start/open all helper classes...
+ m_context.start();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void stop(BundleContext context) throws Exception {
+ if (m_context != null) {
+ // Stop/close all helper classes...
+ m_context.stop();
+ }
+ }
+
+ /**
+ * Creates a new service context in which all helper classes are kept.
+ *
+ * @param context the bundle context to use, cannot be <code>null</code>.
+ * @return a new, initialized, ServiceContext instance, never <code>null</code>.
+ */
+ private ServiceContext createServiceContext(BundleContext context) {
+ // Create all services...
+ EventAdminHelper eventAdmin = new EventAdminHelper(context);
+ UserAdminListenerListHelper listenerList = new UserAdminListenerListHelper(context);
+ EventDispatcher eventDispatcher = new EventDispatcher(eventAdmin, listenerList);
+ RoleRepositoryStoreHelper store = new RoleRepositoryStoreHelper(context);
+
+ RoleRepository roleRepository = new RoleRepository(store);
+
+ return new ServiceContext(eventAdmin, listenerList, eventDispatcher, roleRepository, store);
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/EventAdminHelper.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/EventAdminHelper.java
new file mode 100644
index 0000000..fc73e3f
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/EventAdminHelper.java
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.osgi;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Wraps a {@link ServiceTracker} as an {@link EventAdmin} allowing it to be
+ * used as a "normal" service without having to worry about the actual presence
+ * of the {@link EventAdmin} service itself.
+ */
+final class EventAdminHelper extends ServiceTracker implements EventAdmin {
+
+ /**
+ * Creates a new {@link EventAdminHelper} instance.
+ *
+ * @param context the bundle context to use, cannot be <code>null</code>.
+ */
+ public EventAdminHelper(BundleContext context) {
+ super(context, EventAdmin.class.getName(), null /* customizer */);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void postEvent(Event event) {
+ EventAdmin eventAdmin = getEventAdmin();
+ if (eventAdmin != null) {
+ eventAdmin.postEvent(event);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void sendEvent(Event event) {
+ EventAdmin eventAdmin = getEventAdmin();
+ if (eventAdmin != null) {
+ eventAdmin.sendEvent(event);
+ }
+ }
+
+ /**
+ * Returns the event admin service, if available.
+ *
+ * @return the current event admin service, or <code>null</code> if it is not present.
+ */
+ private EventAdmin getEventAdmin() {
+ return (EventAdmin) getService();
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/RoleRepositoryStoreHelper.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/RoleRepositoryStoreHelper.java
new file mode 100644
index 0000000..d817ca7
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/RoleRepositoryStoreHelper.java
@@ -0,0 +1,123 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.osgi;
+
+import java.io.IOException;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.useradmin.Role;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Provides an OSGi service tracker for {@link RoleRepositoryStore}.
+ * <p>
+ * This helper allows us to use {@link RoleRepositoryStore} without having to
+ * worry about the possible absence of the actual store implementation.
+ * </p>
+ */
+class RoleRepositoryStoreHelper extends ServiceTracker implements RoleRepositoryStore {
+
+ /**
+ * Creates a new {@link RoleRepositoryStoreHelper} instance.
+ *
+ * @param context the bundle context to use, cannot be <code>null</code>.
+ */
+ public RoleRepositoryStoreHelper(BundleContext context) {
+ super(context, RoleRepositoryStore.class.getName(), null /* customizer */);
+ }
+
+ public boolean addRole(Role role) throws IOException {
+ RoleRepositoryStore store = getStore();
+ if (store != null) {
+ return store.addRole(role);
+ }
+
+ return false;
+ }
+
+ public synchronized void close() {
+ try {
+ RoleRepositoryStore store = getStore();
+ if (store != null) {
+ store.close();
+ }
+ }
+ catch (IOException e) {
+ // Ignore; nothing we can do about this here...
+ } finally {
+ super.close();
+ }
+ }
+
+ public Role[] getAllRoles() throws IOException {
+ RoleRepositoryStore store = getStore();
+ if (store != null) {
+ return store.getAllRoles();
+ }
+
+ return new Role[0];
+ }
+
+ public Role getRoleByName(String roleName) throws IOException {
+ RoleRepositoryStore store = getStore();
+ if (store != null) {
+ return store.getRoleByName(roleName);
+ }
+
+ return null;
+ }
+
+ public void initialize() throws IOException {
+ RoleRepositoryStore store = getStore();
+ if (store != null) {
+ store.initialize();
+ }
+ }
+
+ public void removedService(ServiceReference reference, Object service) {
+ RoleRepositoryStore removedStore = (RoleRepositoryStore) service;
+ try {
+ removedStore.close();
+ }
+ catch (IOException e) {
+ // Ignore; nothing we can do about this here...
+ }
+
+ super.removedService(reference, service);
+ }
+
+ public boolean removeRole(Role role) throws IOException {
+ // and possibly also from our tracked store...
+ RoleRepositoryStore store = getStore();
+ if (store != null) {
+ return store.removeRole(role);
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the tracked {@link RoleRepositoryStore}.
+ *
+ * @return the {@link RoleRepositoryStore}, can be <code>null</code>.
+ */
+ private RoleRepositoryStore getStore() {
+ return (RoleRepositoryStore) getService();
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/ServiceContext.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/ServiceContext.java
new file mode 100644
index 0000000..1d87ffd
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/ServiceContext.java
@@ -0,0 +1,68 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.osgi;
+
+
+import org.apache.felix.useradmin.impl.EventDispatcher;
+import org.apache.felix.useradmin.impl.RoleRepository;
+import org.osgi.service.useradmin.UserAdmin;
+
+/**
+ * Provides a convenience class for all helpers that are used for this
+ * implementation of the {@link UserAdmin} service.
+ */
+final class ServiceContext {
+
+ final EventAdminHelper m_eventAdmin;
+ final UserAdminListenerListHelper m_listenerList;
+ final EventDispatcher m_eventDispatcher;
+ final RoleRepository m_roleRepository;
+ final RoleRepositoryStoreHelper m_store;
+
+ /**
+ * Creates a new ServiceContext instance.
+ */
+ public ServiceContext(EventAdminHelper eventAdmin, UserAdminListenerListHelper listenerList, EventDispatcher eventDispatcher, RoleRepository roleRepository, RoleRepositoryStoreHelper store) {
+ m_eventAdmin = eventAdmin;
+ m_listenerList = listenerList;
+ m_eventDispatcher = eventDispatcher;
+ m_roleRepository = roleRepository;
+ m_store = store;
+ }
+
+ /**
+ * Starts/opens all helpers.
+ */
+ public void start() {
+ m_eventAdmin.open();
+ m_listenerList.open(true /* trackAllServices */);
+ m_eventDispatcher.start();
+ m_roleRepository.start();
+ m_store.open();
+ }
+
+ /**
+ * Stops/closes all helpers.
+ */
+ public void stop() {
+ m_roleRepository.stop();
+ m_eventDispatcher.stop();
+ m_listenerList.close();
+ m_eventAdmin.close();
+ m_store.close();
+ }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/UserAdminListenerListHelper.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/UserAdminListenerListHelper.java
new file mode 100644
index 0000000..1b19f74
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/UserAdminListenerListHelper.java
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.osgi;
+
+
+import org.apache.felix.useradmin.impl.UserAdminListenerList;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.useradmin.UserAdminListener;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Wraps a {@link ServiceTracker} as an {@link UserAdminListenerList} to allow
+ * it to be used as a "normal" service without having to worry about the
+ * existence of any {@link UserAdminListener}s.
+ */
+final class UserAdminListenerListHelper extends ServiceTracker implements UserAdminListenerList {
+
+ /**
+ * Creates a new {@link UserAdminListenerListHelper} instance.
+ *
+ * @param context the bundle context to use, cannot be <code>null</code>.
+ */
+ public UserAdminListenerListHelper(BundleContext context) {
+ super(context, UserAdminListener.class.getName(), null /* customizer */);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public UserAdminListener[] getListeners() {
+ Object[] services = getServices();
+ if (services == null) {
+ return new UserAdminListener[0];
+ }
+
+ UserAdminListener[] result = new UserAdminListener[services.length];
+ for (int i = 0; i < services.length; i++) {
+ result[i] = (UserAdminListener) services[i];
+ }
+
+ return result;
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/AuthorizationImplTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/AuthorizationImplTest.java
new file mode 100644
index 0000000..7cc1c39
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/AuthorizationImplTest.java
@@ -0,0 +1,285 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Group;
+
+/**
+ * Test cases for {@link AuthorizationImpl}.
+ */
+public class AuthorizationImplTest extends TestCase {
+
+ private RoleRepository m_roleManager;
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_roleManager = new RoleRepository(new MemoryRoleRepositoryStore());
+ }
+
+ /**
+ * Test for example presented in section 107.3.2 of OSGi compendium v4.2.
+ */
+ public void testAuthorizationExampleOk() {
+ // Action groups...
+ Group alarmSystemControl = createGroup("AlarmSystemControl");
+ Group internetAccess = createGroup("InternetAccess");
+ Group temperatureControl = createGroup("TemperatureControl");
+ Group photoAlbumEdit = createGroup("PhotoAlbumEdit");
+ Group photoAlbumView = createGroup("PhotoAlbumView");
+ Group portForwarding = createGroup("PortForwarding");
+
+ // System user groups...
+ Group administrators = createGroup("Administrators");
+ Group buddies = createGroup("Buddies");
+ Group children = createGroup("Children");
+ Group adults = createGroup("Adults");
+ Group residents = createGroup("Residents");
+
+ // Users
+ UserImpl elmer = new UserImpl("Elmer");
+ UserImpl fudd = new UserImpl("Fudd");
+ UserImpl marvin = new UserImpl("Marvin");
+ UserImpl pepe = new UserImpl("Pepe");
+ UserImpl daffy = new UserImpl("Daffy");
+ UserImpl foghorn = new UserImpl("Foghorn");
+
+ // Not explicitly mentioned; but needed to comply with the semantics
+ alarmSystemControl.addRequiredMember(RoleRepository.USER_ANYONE);
+ internetAccess.addRequiredMember(RoleRepository.USER_ANYONE);
+ temperatureControl.addRequiredMember(RoleRepository.USER_ANYONE);
+ photoAlbumEdit.addRequiredMember(RoleRepository.USER_ANYONE);
+ photoAlbumView.addRequiredMember(RoleRepository.USER_ANYONE);
+ portForwarding.addRequiredMember(RoleRepository.USER_ANYONE);
+
+ administrators.addRequiredMember(RoleRepository.USER_ANYONE);
+ buddies.addRequiredMember(RoleRepository.USER_ANYONE);
+ children.addRequiredMember(RoleRepository.USER_ANYONE);
+ adults.addRequiredMember(RoleRepository.USER_ANYONE);
+ residents.addRequiredMember(RoleRepository.USER_ANYONE);
+
+ // Table 107.1
+ residents.addMember(elmer);
+ residents.addMember(fudd);
+ residents.addMember(marvin);
+ residents.addMember(pepe);
+
+ buddies.addMember(daffy);
+ buddies.addMember(foghorn);
+
+ children.addMember(marvin);
+ children.addMember(pepe);
+
+ adults.addMember(elmer);
+ adults.addMember(fudd);
+
+ administrators.addMember(elmer);
+
+ // Table 107.2
+ alarmSystemControl.addMember(residents);
+ alarmSystemControl.addRequiredMember(administrators);
+
+ internetAccess.addMember(residents);
+ internetAccess.addRequiredMember(adults);
+
+ temperatureControl.addMember(residents);
+ temperatureControl.addRequiredMember(adults);
+
+ photoAlbumEdit.addMember(residents);
+ photoAlbumEdit.addMember(children);
+ photoAlbumEdit.addMember(adults);
+
+ photoAlbumView.addMember(residents);
+ photoAlbumView.addMember(buddies);
+
+ portForwarding.addMember(residents);
+ portForwarding.addRequiredMember(administrators);
+
+ // Test with the user "foghorn"...
+ AuthorizationImpl auth = new AuthorizationImpl(foghorn, m_roleManager);
+
+ assertFalse(auth.hasRole(alarmSystemControl.getName()));
+ assertFalse(auth.hasRole(internetAccess.getName()));
+ assertFalse(auth.hasRole(temperatureControl.getName()));
+ assertFalse(auth.hasRole(photoAlbumEdit.getName()));
+ assertTrue(auth.hasRole(photoAlbumView.getName()));
+ assertFalse(auth.hasRole(portForwarding.getName()));
+
+ // Test with the user "fudd"...
+ auth = new AuthorizationImpl(fudd, m_roleManager);
+
+ assertFalse(auth.hasRole(alarmSystemControl.getName()));
+ assertTrue(auth.hasRole(internetAccess.getName()));
+ assertTrue(auth.hasRole(temperatureControl.getName()));
+ assertTrue(auth.hasRole(photoAlbumEdit.getName()));
+ assertTrue(auth.hasRole(photoAlbumView.getName()));
+ assertFalse(auth.hasRole(portForwarding.getName()));
+
+ // Test with the user "elmer"...
+ auth = new AuthorizationImpl(elmer, m_roleManager);
+
+ assertTrue(auth.hasRole(alarmSystemControl.getName()));
+ assertTrue(auth.hasRole(internetAccess.getName()));
+ assertTrue(auth.hasRole(temperatureControl.getName()));
+ assertTrue(auth.hasRole(photoAlbumEdit.getName()));
+ assertTrue(auth.hasRole(photoAlbumView.getName()));
+ assertTrue(auth.hasRole(portForwarding.getName()));
+ }
+
+ /**
+ * Test that the tests for membership work correctly.
+ */
+ public void testHasRoleOk() {
+ GroupImpl citizens = createGroup("citizen");
+ citizens.addRequiredMember(RoleRepository.USER_ANYONE);
+
+ GroupImpl adults = createGroup("adult");
+ adults.addRequiredMember(RoleRepository.USER_ANYONE);
+
+ GroupImpl voters = createGroup("voter");
+ voters.addRequiredMember(citizens);
+ voters.addRequiredMember(adults);
+ voters.addMember(RoleRepository.USER_ANYONE);
+
+ // Elmer belongs to the citizens and adults...
+ UserImpl elmer = createUser("elmer");
+ citizens.addMember(elmer);
+ adults.addMember(elmer);
+
+ // Pepe belongs to the citizens, but is not an adult...
+ UserImpl pepe = createUser("pepe");
+ citizens.addMember(pepe);
+
+ // Bugs is an adult, but is not a citizen...
+ UserImpl bugs = createUser("bugs");
+ adults.addMember(bugs);
+
+ // Daffy is not an adult, neither a citizen...
+ UserImpl daffy = createUser("daffy");
+
+ AuthorizationImpl auth;
+
+ auth = new AuthorizationImpl(elmer, m_roleManager);
+ assertTrue(auth.hasRole("adult"));
+ assertTrue(auth.hasRole("citizen"));
+ assertTrue(auth.hasRole("voter"));
+ assertFalse(auth.hasRole("non-existing-role"));
+
+ auth = new AuthorizationImpl(pepe, m_roleManager);
+ assertFalse(auth.hasRole("adult"));
+ assertTrue(auth.hasRole("citizen"));
+ assertFalse(auth.hasRole("voter"));
+ assertFalse(auth.hasRole("non-existing-role"));
+
+ auth = new AuthorizationImpl(bugs, m_roleManager);
+ assertTrue(auth.hasRole("adult"));
+ assertFalse(auth.hasRole("citizen"));
+ assertFalse(auth.hasRole("voter"));
+ assertFalse(auth.hasRole("non-existing-role"));
+
+ auth = new AuthorizationImpl(daffy, m_roleManager);
+ assertFalse(auth.hasRole("adult"));
+ assertFalse(auth.hasRole("citizen"));
+ assertFalse(auth.hasRole("voter"));
+ assertFalse(auth.hasRole("non-existing-role"));
+ }
+
+ /**
+ * Test that the tests for membership work correctly.
+ */
+ public void testGetRolesOk() {
+ GroupImpl citizens = createGroup("citizen");
+ citizens.addRequiredMember(RoleRepository.USER_ANYONE);
+
+ GroupImpl adults = createGroup("adult");
+ adults.addRequiredMember(RoleRepository.USER_ANYONE);
+
+ GroupImpl voters = createGroup("voter");
+ voters.addRequiredMember(citizens);
+ voters.addRequiredMember(adults);
+ voters.addMember(RoleRepository.USER_ANYONE);
+
+ // Elmer belongs to the citizens and adults...
+ UserImpl elmer = createUser("elmer");
+ citizens.addMember(elmer);
+ adults.addMember(elmer);
+
+ // Pepe belongs to the citizens, but is not an adult...
+ UserImpl pepe = createUser("pepe");
+ citizens.addMember(pepe);
+
+ // Bugs is an adult, but is not a citizen...
+ UserImpl bugs = createUser("bugs");
+ adults.addMember(bugs);
+
+ // Daffy is not an adult, neither a citizen...
+ UserImpl daffy = createUser("daffy");
+
+ // Daffy is not an adult, neither a citizen...
+ UserImpl donald = new UserImpl("donald");
+
+ AuthorizationImpl auth;
+
+ auth = new AuthorizationImpl(elmer, m_roleManager);
+ assertSameRoles(new String[]{ "elmer", "adult", "citizen", "voter" }, auth.getRoles());
+
+ auth = new AuthorizationImpl(pepe, m_roleManager);
+ assertSameRoles(new String[]{ "pepe", "citizen" }, auth.getRoles());
+
+ auth = new AuthorizationImpl(bugs, m_roleManager);
+ assertSameRoles(new String[]{ "bugs", "adult" }, auth.getRoles());
+
+ auth = new AuthorizationImpl(daffy, m_roleManager);
+ assertSameRoles(new String[]{ "daffy" }, auth.getRoles());
+
+ auth = new AuthorizationImpl(donald, m_roleManager);
+ assertNull(auth.getRoles());
+ }
+
+ private void assertSameRoles(String[] expected, String[] roles) {
+ assertTrue("Expected " + expected.length + " roles, got " + roles.length + "!", expected.length == roles.length);
+
+ List e = new ArrayList(Arrays.asList(expected));
+ List r = new ArrayList(Arrays.asList(roles));
+ e.removeAll(r);
+
+ assertTrue("Not seen: " + e, e.isEmpty());
+ }
+
+ private GroupImpl createGroup(String name) {
+ GroupImpl result = new GroupImpl(name);
+ m_roleManager.addRole(result);
+ return result;
+ }
+
+ private UserImpl createUser(String name) {
+ UserImpl result = new UserImpl(name);
+ m_roleManager.addRole(result);
+ return result;
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/EventDispatcherTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/EventDispatcherTest.java
new file mode 100644
index 0000000..4f89d12
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/EventDispatcherTest.java
@@ -0,0 +1,296 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl;
+
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import junit.framework.TestCase;
+
+
+import org.apache.felix.useradmin.impl.EventDispatcher;
+import org.apache.felix.useradmin.impl.UserAdminListenerList;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Test case for {@link EventDispatcher}.
+ */
+public class EventDispatcherTest extends TestCase {
+
+ /**
+ * Provides a {@link EventAdmin} that counts the number of times
+ * {@link #postEvent(Event)} is called.
+ */
+ static class CountingEventAdmin implements EventAdmin {
+
+ private final CountDownLatch m_latch;
+
+ public CountingEventAdmin(CountDownLatch latch) {
+ m_latch = latch;
+ }
+
+ public void postEvent(Event event) {
+ if (m_latch != null) {
+ m_latch.countDown();
+ }
+ }
+
+ public void sendEvent(Event event) {
+ throw new RuntimeException("Should not be called for asynchronous delivery!");
+ }
+ }
+
+ /**
+ * Provides a {@link UserAdminListener} that counts the number of times
+ * {@link #roleChanged(UserAdminEvent)} is called.
+ */
+ static class CountingUserAdminListener implements UserAdminListener {
+
+ private final CountDownLatch m_latch;
+
+ public CountingUserAdminListener(CountDownLatch latch) {
+ m_latch = latch;
+ }
+
+ public void roleChanged(UserAdminEvent event) {
+ if (m_latch != null) {
+ m_latch.countDown();
+ }
+ }
+ }
+
+ /**
+ * Implements a fake service reference.
+ */
+ static class FakeServiceReference implements ServiceReference {
+
+ private final Properties m_props;
+
+ public FakeServiceReference() {
+ this(new String[0]);
+ }
+
+ public FakeServiceReference(String[] properties) {
+ m_props = new Properties();
+
+ for (int i = 0; i < properties.length; i += 2) {
+ m_props.put(properties[i], properties[i + 1]);
+ }
+ }
+
+ public Bundle getBundle() {
+ return null;
+ }
+
+ public Object getProperty(String key) {
+ return key;
+ }
+
+ public String[] getPropertyKeys() {
+ return new String[0];
+ }
+
+ public Bundle[] getUsingBundles() {
+ return null;
+ }
+
+ public boolean isAssignableTo(Bundle bundle, String className) {
+ return false;
+ }
+ }
+
+ private EventDispatcher m_dispatcher;
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#dispatch(org.osgi.service.useradmin.UserAdminEvent)}.
+ */
+ public void testDispatchEventCallsEventAdminOk() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(5);
+
+ m_dispatcher = createEventDispatcher(new CountingEventAdmin(latch));
+ m_dispatcher.start();
+
+ for (int i = 0; i < 5; i++) {
+ m_dispatcher.dispatch(createMockEvent(UserAdminEvent.ROLE_CHANGED));
+ }
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#dispatch(org.osgi.service.useradmin.UserAdminEvent)}.
+ */
+ public void testDispatchEventCallsUserAdminListenersOk() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(5);
+
+ m_dispatcher = createEventDispatcher(new CountingUserAdminListener(latch));
+ m_dispatcher.start();
+
+ for (int i = 0; i < 5; i++) {
+ m_dispatcher.dispatch(createMockEvent(UserAdminEvent.ROLE_CHANGED));
+ }
+
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#start()}.
+ */
+ public void testStartTwiceDoesNotMatter() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ m_dispatcher = createEventDispatcher(new CountingUserAdminListener(latch));
+ m_dispatcher.start();
+
+ assertTrue(m_dispatcher.isRunning());
+
+ m_dispatcher.start();
+
+ assertTrue(m_dispatcher.isRunning());
+
+ assertEquals("Latch should not be decremented?!", 1L, latch.getCount());
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#stop()}.
+ */
+ public void testStopOk() {
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ m_dispatcher = createEventDispatcher(new CountingUserAdminListener(latch));
+ m_dispatcher.start();
+
+ m_dispatcher.stop();
+
+ m_dispatcher.dispatch(createMockEvent(UserAdminEvent.ROLE_CHANGED));
+
+ assertFalse(m_dispatcher.isRunning());
+
+ assertEquals("Latch should not be decremented?!", 1L, latch.getCount());
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#stop()}.
+ */
+ public void testStopUnstartedDoesNotMatter() {
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ m_dispatcher = createEventDispatcher(new CountingUserAdminListener(latch));
+
+ assertFalse(m_dispatcher.isRunning());
+
+ m_dispatcher.stop();
+
+ assertFalse(m_dispatcher.isRunning());
+
+ assertEquals("Latch should not be decremented?!", 1L, latch.getCount());
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#stop()}.
+ */
+ public void testStopTwiceDoesNotMatter() {
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ m_dispatcher = createEventDispatcher(new CountingUserAdminListener(latch));
+ m_dispatcher.start();
+
+ assertTrue(m_dispatcher.isRunning());
+
+ m_dispatcher.stop();
+
+ assertFalse(m_dispatcher.isRunning());
+
+ m_dispatcher.stop();
+
+ assertEquals("Latch should not be decremented?!", 1L, latch.getCount());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void tearDown() throws Exception {
+ if (m_dispatcher != null) {
+ try {
+ m_dispatcher.stop();
+ m_dispatcher = null;
+ } catch (IllegalStateException e) {
+ // Not a problem; already stopped...
+ }
+ }
+ }
+
+ /**
+ * @param eventAdmin
+ * @param listenerList
+ * @return
+ */
+ private EventDispatcher createEventDispatcher(EventAdmin eventAdmin) {
+ return new EventDispatcher(eventAdmin, createListenerList());
+ }
+
+ /**
+ * @param eventAdmin
+ * @param listenerList
+ * @return
+ */
+ private EventDispatcher createEventDispatcher(UserAdminListener listener) {
+ return new EventDispatcher(new CountingEventAdmin(null), createListenerList(listener));
+ }
+
+ /**
+ * @param listener
+ * @return
+ */
+ private UserAdminListenerList createListenerList() {
+ return new UserAdminListenerList() {
+ public UserAdminListener[] getListeners() {
+ return new UserAdminListener[0];
+ }
+ };
+ }
+
+ /**
+ * @param listener
+ * @return
+ */
+ private UserAdminListenerList createListenerList(final UserAdminListener listener) {
+ return new UserAdminListenerList() {
+ public UserAdminListener[] getListeners() {
+ return new UserAdminListener[] { listener };
+ }
+ };
+ }
+
+ /**
+ * @param type
+ * @return
+ */
+ private UserAdminEvent createMockEvent(int type) {
+ Role user = new UserImpl("user-" + System.currentTimeMillis());
+ ServiceReference ref = new FakeServiceReference();
+ return new UserAdminEvent(ref, type, user);
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/MemoryRoleRepositoryStore.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/MemoryRoleRepositoryStore.java
new file mode 100644
index 0000000..8a0b7c3
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/MemoryRoleRepositoryStore.java
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.osgi.service.useradmin.Role;
+
+
+/**
+ * Provides a simple in-memory role repository store.
+ */
+public class MemoryRoleRepositoryStore implements RoleRepositoryStore {
+
+ protected final ConcurrentMap m_entries = new ConcurrentHashMap();
+
+ public boolean addRole(Role role) throws IOException {
+ if (role == null) {
+ throw new IllegalArgumentException("Role cannot be null!");
+ }
+ Object result = m_entries.putIfAbsent(role.getName(), role);
+ return result == null;
+ }
+
+ public void close() throws IOException {
+ // Nop
+ }
+
+ public void flush() throws IOException {
+ // Nop
+ }
+
+ public Role[] getAllRoles() throws IOException {
+ Collection roles = m_entries.values();
+ Role[] result = new Role[roles.size()];
+ return (Role[]) roles.toArray(result);
+ }
+
+ public Role getRoleByName(String roleName) throws IOException {
+ if (roleName == null) {
+ throw new IllegalArgumentException("Role name cannot be null!");
+ }
+ return (Role) m_entries.get(roleName);
+ }
+
+ public void initialize() throws IOException {
+ // Nop
+ }
+
+ public boolean removeRole(Role role) throws IOException {
+ if (role == null) {
+ throw new IllegalArgumentException("Role cannot be null!");
+ }
+ return m_entries.remove(role.getName(), role);
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleCheckerTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleCheckerTest.java
new file mode 100644
index 0000000..191f6f1
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleCheckerTest.java
@@ -0,0 +1,214 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl;
+
+import org.apache.felix.useradmin.impl.RoleChecker;
+import org.apache.felix.useradmin.impl.RoleRepository;
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Test cases for {@link RoleChecker}.
+ */
+public class RoleCheckerTest extends TestCase {
+
+ private RoleChecker m_roleChecker;
+
+ /**
+ * Tests that a user always implies itself.
+ */
+ public void testUserAlwaysImpliesItself() {
+ UserImpl user = new UserImpl("foo");
+
+ assertTrue(m_roleChecker.isImpliedBy(user, user));
+ }
+
+ /**
+ * Test that a user does never imply a group to which it is not a member.
+ */
+ public void testUserDoesNotImplyNotImpliedGroup() {
+ UserImpl user = new UserImpl("foo");
+ GroupImpl group = new GroupImpl("bar");
+
+ assertFalse(m_roleChecker.isImpliedBy(user, group));
+ }
+
+ /**
+ * Test that a user does never imply a group to which it is not a member.
+ */
+ public void testUserImpliesImpliedGroup() {
+ UserImpl user = new UserImpl("foo");
+
+ GroupImpl group = new GroupImpl("bar");
+ group.addRequiredMember(RoleRepository.USER_ANYONE);
+ group.addMember(user);
+
+ assertTrue(m_roleChecker.isImpliedBy(group, user));
+ }
+
+ /**
+ * Test that a user does never imply a group to which it is not a member.
+ */
+ public void testGroupDoesNotImplyNotImpliedUser() {
+ UserImpl user = new UserImpl("foo");
+
+ GroupImpl group = new GroupImpl("bar");
+ group.addMember(user);
+
+ assertFalse(m_roleChecker.isImpliedBy(user, group));
+ }
+
+ /**
+ * Test that a group does never imply a group to which it is a required member.
+ */
+ public void testGroupDoesNotImplySameRequiredGroup() {
+ UserImpl user = new UserImpl("foo");
+
+ GroupImpl group = new GroupImpl("bar");
+ group.addRequiredMember(group);
+ group.addMember(user);
+
+ assertFalse(m_roleChecker.isImpliedBy(group, group));
+ }
+
+ /**
+ * Test that a group does never imply a group to which it is a basic member.
+ */
+ public void testGroupDoesNotImplySameGroup() {
+ UserImpl user = new UserImpl("foo");
+
+ GroupImpl group = new GroupImpl("bar");
+ group.addMember(group);
+ group.addMember(user);
+
+ assertFalse(m_roleChecker.isImpliedBy(group, group));
+ }
+
+ /**
+ * Test that a membership can be implied for users belonging to multiple required groups.
+ */
+ public void testRequiredRolesMultipleRequiredGroupsOk() {
+ UserImpl elmer = new UserImpl("elmer");
+ UserImpl pepe = new UserImpl("pepe");
+ UserImpl bugs = new UserImpl("bugs");
+ UserImpl daffy = new UserImpl("daffy");
+
+ GroupImpl administrators = new GroupImpl("administrators");
+ administrators.addRequiredMember(RoleRepository.USER_ANYONE);
+ administrators.addMember(elmer);
+ administrators.addMember(pepe);
+ administrators.addMember(bugs);
+
+ GroupImpl family = new GroupImpl("family");
+ family.addRequiredMember(RoleRepository.USER_ANYONE);
+ family.addMember(elmer);
+ family.addMember(pepe);
+ family.addMember(daffy);
+
+ GroupImpl alarmSystemActivation = new GroupImpl("alarmSystemActivation");
+ alarmSystemActivation.addMember(RoleRepository.USER_ANYONE);
+ alarmSystemActivation.addRequiredMember(administrators);
+ alarmSystemActivation.addRequiredMember(family);
+
+ assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, elmer));
+ assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, pepe));
+ assertFalse(m_roleChecker.isImpliedBy(alarmSystemActivation, bugs));
+ assertFalse(m_roleChecker.isImpliedBy(alarmSystemActivation, daffy));
+ }
+
+ /**
+ * Test that a membership can be implied for users belonging to multiple non-required groups.
+ */
+ public void testRequiredRolesMultipleGroupsOk() {
+ UserImpl elmer = new UserImpl("elmer");
+ UserImpl pepe = new UserImpl("pepe");
+ UserImpl bugs = new UserImpl("bugs");
+ UserImpl daffy = new UserImpl("daffy");
+
+ GroupImpl administrators = new GroupImpl("administrators");
+ administrators.addRequiredMember(RoleRepository.USER_ANYONE);
+ administrators.addMember(elmer);
+ administrators.addMember(pepe);
+ administrators.addMember(bugs);
+
+ GroupImpl family = new GroupImpl("family");
+ family.addRequiredMember(RoleRepository.USER_ANYONE);
+ family.addMember(elmer);
+ family.addMember(pepe);
+ family.addMember(daffy);
+
+ GroupImpl alarmSystemActivation = new GroupImpl("alarmSystemActivation");
+ alarmSystemActivation.addRequiredMember(RoleRepository.USER_ANYONE);
+ alarmSystemActivation.addMember(administrators);
+ alarmSystemActivation.addMember(family);
+
+ assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, elmer));
+ assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, pepe));
+ assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, bugs));
+ assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, daffy));
+ }
+
+ /**
+ * Test that a membership can be implied for users belonging to multiple non-required groups.
+ */
+ public void testVotersRequiredMembersOk() {
+ GroupImpl citizens = new GroupImpl("citizen");
+ citizens.addRequiredMember(RoleRepository.USER_ANYONE);
+
+ GroupImpl adults = new GroupImpl("adult");
+ adults.addRequiredMember(RoleRepository.USER_ANYONE);
+
+ GroupImpl voters = new GroupImpl("voter");
+ voters.addRequiredMember(citizens);
+ voters.addRequiredMember(adults);
+ voters.addMember(RoleRepository.USER_ANYONE);
+
+
+ // Elmer belongs to the citizens and adults...
+ UserImpl elmer = new UserImpl("elmer");
+ citizens.addMember(elmer);
+ adults.addMember(elmer);
+
+ // Pepe belongs to the citizens, but is not an adult...
+ UserImpl pepe = new UserImpl("pepe");
+ citizens.addMember(pepe);
+
+ // Bugs is an adult, but is not a citizen...
+ UserImpl bugs = new UserImpl("bugs");
+ adults.addMember(bugs);
+
+ // Daffy is not an adult, neither a citizen...
+ UserImpl daffy = new UserImpl("daffy");
+
+ assertTrue(m_roleChecker.isImpliedBy(voters, elmer));
+ assertFalse(m_roleChecker.isImpliedBy(voters, pepe));
+ assertFalse(m_roleChecker.isImpliedBy(voters, bugs));
+ assertFalse(m_roleChecker.isImpliedBy(voters, daffy));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_roleChecker = new RoleChecker();
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositorySecurityTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositorySecurityTest.java
new file mode 100644
index 0000000..c6f1185
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositorySecurityTest.java
@@ -0,0 +1,143 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.impl;
+
+import java.security.Permission;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.framework.FilterImpl;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Test cases for {@link RoleRepository}.
+ */
+public class RoleRepositorySecurityTest extends TestCase {
+
+ private RoleRepository m_roleManager;
+ private TestSecurityManager m_securityManager;
+
+ /**
+ * Tests that adding a role with the right permissions works.
+ */
+ public void testAddRoleWithPermissionsOk() throws SecurityException {
+ m_securityManager.m_allowed = true;
+
+ m_roleManager.addRole(RoleRepository.USER_ANYONE);
+ }
+
+ /**
+ * Tests that adding a role without the right permissions does not work.
+ */
+ public void testAddRoleWithoutPermissionsFails() throws SecurityException {
+ try {
+ m_roleManager.addRole(RoleRepository.USER_ANYONE);
+
+ fail("Expected SecurityException!");
+ } catch (SecurityException e) {
+ // Ok; expected...
+ }
+ }
+
+ /**
+ * Tests that getting roles by their name works without any permissions.
+ */
+ public void testGetRoleByNameOk() throws SecurityException {
+ m_roleManager.getRoleByName(Role.USER_ANYONE);
+ }
+
+ /**
+ * Tests that getting roles by key-value pairs works without any permissions.
+ */
+ public void testGetRolesWithKeyValueOk() throws SecurityException {
+ m_roleManager.getRoles("key", "value");
+ }
+
+ /**
+ * Tests that getting roles with filters works without any permissions.
+ */
+ public void testGetRolesWithFilterOk() throws Exception {
+ m_roleManager.getRoles(new FilterImpl("(key=value)"));
+ }
+
+ /**
+ * Tests that removing a role with the right permissions works.
+ */
+ public void testRemoveRoleWithPermissionsOk() throws SecurityException {
+ m_securityManager.m_allowed = true;
+
+ m_roleManager.removeRole(RoleRepository.USER_ANYONE);
+ }
+
+ /**
+ * Tests that remove a role without the right permissions does not work.
+ */
+ public void testRemoveRoleWithoutPermissionsFails() throws SecurityException {
+ try {
+ m_roleManager.removeRole(RoleRepository.USER_ANYONE);
+
+ fail("Expected SecurityException!");
+ } catch (SecurityException e) {
+ // Ok; expected...
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_roleManager = new RoleRepository(new MemoryRoleRepositoryStore());
+
+ m_securityManager = new TestSecurityManager();
+ System.setSecurityManager(m_securityManager);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ System.setSecurityManager(null);
+ }
+
+ /**
+ * Provides a test security manager.
+ */
+ static final class TestSecurityManager extends SecurityManager {
+
+ volatile boolean m_allowed = false;
+
+ public void checkPermission(Permission perm) {
+ // Cannot use instanceof as it requires a special permission as well...
+ if ("UserAdminPermission".equals(perm.getClass().getSimpleName())) {
+ String name = perm.getName();
+ if ("admin".equals(name) && !m_allowed) {
+ throw new SecurityException("Not allowed!");
+ }
+ }
+ // Do not check for other permissions...
+ }
+
+ public void checkPermission(Permission perm, Object context) {
+ // Do not check for other permissions...
+ }
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositoryTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositoryTest.java
new file mode 100644
index 0000000..1003bfa
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositoryTest.java
@@ -0,0 +1,570 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.framework.FilterImpl;
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.framework.Filter;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Test cases for {@link RoleRepository}.
+ */
+public class RoleRepositoryTest extends TestCase {
+
+ private RoleRepository m_roleManager;
+ private CountDownLatch m_latch;
+
+ /**
+ * Tests whether adding a new role to a group causes an event to be emitted to the {@link RoleRepository}.
+ */
+ public void testAddBasicRoleYieldsEventOk() throws Exception {
+ final GroupImpl role = (GroupImpl) m_roleManager.addRole(new GroupImpl("foo"));
+
+ m_latch = new CountDownLatch(1);
+
+ new Thread(new Runnable() {
+ public void run() {
+ role.addMember(RoleRepository.USER_ANYONE);
+ };
+ }).start();
+
+ assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Tests that adding a null {@link RoleChangeListener} instance does not work.
+ */
+ public void testAddNullRoleChangeListenerFail() throws Exception {
+ try {
+ m_roleManager.addRoleChangeListener(null);
+ fail("Expected IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected...
+ }
+ }
+
+ /**
+ * Tests that adding a predefined role is not allowed.
+ */
+ public void testAddPredefineRoleFails() {
+ Role role = RoleRepository.USER_ANYONE;
+ assertNull(m_roleManager.addRole(role));
+ assertEquals(0, m_roleManager.getRoles(null).size());
+ }
+
+ /**
+ * Tests whether adding a new role to a group causes an event to be emitted to the {@link RoleRepository}.
+ */
+ public void testAddRequiredRoleYieldsEventOk() throws Exception {
+ final GroupImpl role = (GroupImpl) m_roleManager.addRole(new GroupImpl("foo"));
+
+ m_latch = new CountDownLatch(1);
+
+ new Thread(new Runnable() {
+ public void run() {
+ role.addRequiredMember(RoleRepository.USER_ANYONE);
+ };
+ }).start();
+
+ assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Tests that adding a valid {@link RoleChangeListener} instance works.
+ */
+ public void testAddRoleChangeListenerOk() throws Exception {
+ // Should succeed...
+ m_roleManager.addRoleChangeListener(new RoleChangeListener() {
+ public void propertyAdded(Role role, Object key, Object value) {
+ }
+
+ public void propertyChanged(Role role, Object key, Object oldValue, Object newValue) {
+ }
+
+ public void propertyRemoved(Role role, Object key) {
+ }
+
+ public void roleAdded(Role role) {
+ }
+
+ public void roleRemoved(Role role) {
+ }
+ });
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#addRole(Role)}.
+ */
+ public void testAddRoleOfSameTypeTwiceFail() {
+ UserImpl role = new UserImpl("foo");
+
+ assertSame(role, m_roleManager.addRole(role));
+ assertEquals(1, m_roleManager.getRoles(null).size());
+
+ assertNull(m_roleManager.addRole(role));
+ assertEquals(1, m_roleManager.getRoles(null).size());
+ }
+
+ /**
+ * Tests that adding a role works.
+ */
+ public void testAddRoleOk() {
+ UserImpl role = new UserImpl("foo");
+ assertSame(role, m_roleManager.addRole(role));
+ assertEquals(1, m_roleManager.getRoles(null).size());
+ }
+
+ /**
+ * Tests whether adding a new property to a role causes an event to be emitted to the {@link RoleRepository}.
+ */
+ public void testAddRolePropertyYieldsEventOk() throws Exception {
+ m_latch = new CountDownLatch(1);
+
+ final Role role = m_roleManager.addRole(new UserImpl("john.doe"));
+
+ new Thread(new Runnable() {
+ public void run() {
+ role.getProperties().put("key", "value");
+ };
+ }).start();
+
+ assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Tests that adding a role that does not inherit from RoleImpl does not work and yields an exception.
+ */
+ public void testAddRoleWithInvalidRoleFail() {
+ try {
+ m_roleManager.addRole(new Role() {
+ public String getName() {
+ return "A User";
+ }
+ public Dictionary getProperties() {
+ return new Properties();
+ }
+ public int getType() {
+ return Role.USER;
+ }
+ });
+
+ fail("Exception expected!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that adding a null-role does not work and yields an exception.
+ */
+ public void testAddRoleWithNullRoleFail() {
+ try {
+ m_roleManager.addRole(null);
+
+ fail("Exception expected!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#addRole(Role)}.
+ */
+ public void testAddRoleWithSameNameTwiceFail() {
+ UserImpl role1 = new UserImpl("foo");
+ GroupImpl role2 = new GroupImpl("foo");
+
+ assertSame(role1, m_roleManager.addRole(role1));
+ assertEquals(1, m_roleManager.getRoles(null).size());
+
+ assertNull(m_roleManager.addRole(role2));
+ assertEquals(1, m_roleManager.getRoles(null).size());
+ }
+
+ /**
+ * Tests whether adding a new credential to a user causes an event to be emitted to the {@link RoleRepository}.
+ */
+ public void testAddUserCredentialYieldsEventOk() throws Exception {
+ m_latch = new CountDownLatch(1);
+
+ final UserImpl role = (UserImpl) m_roleManager.addRole(new UserImpl("john.doe"));
+
+ new Thread(new Runnable() {
+ public void run() {
+ role.getCredentials().put("key", "value");
+ };
+ }).start();
+
+ assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Tests whether changing an existing property to a role causes an event to be emitted to the {@link RoleRepository}.
+ */
+ public void testChangeRolePropertyYieldsEventOk() throws Exception {
+ final Role role = m_roleManager.addRole(new UserImpl("john.doe"));
+ role.getProperties().put("key", "value");
+
+ m_latch = new CountDownLatch(1);
+
+ new Thread(new Runnable() {
+ public void run() {
+ role.getProperties().put("key", "other-value");
+ };
+ }).start();
+
+ assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Tests whether changing an existing credential for a user causes an event to be emitted to the {@link RoleRepository}.
+ */
+ public void testChangeUserCredentialYieldsEventOk() throws Exception {
+ final UserImpl role = (UserImpl) m_roleManager.addRole(new UserImpl("john.doe"));
+ role.getCredentials().put("key", "value");
+
+ m_latch = new CountDownLatch(1);
+
+ new Thread(new Runnable() {
+ public void run() {
+ role.getCredentials().put("key", "other-value");
+ };
+ }).start();
+
+ assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#getRoleByName(java.lang.String)}.
+ */
+ public void testGetRoleByName() {
+ Role role1 = m_roleManager.addRole(new UserImpl("foo"));
+ Role role2 = m_roleManager.addRole(new GroupImpl("bar"));
+
+ assertSame(role1, m_roleManager.getRoleByName("foo"));
+ assertSame(role2, m_roleManager.getRoleByName("bar"));
+ assertNull(m_roleManager.getRoleByName("qux"));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#getRoles(org.osgi.framework.Filter)}.
+ */
+ public void testGetRolesWithFilterOk() throws Exception {
+ Role role1 = m_roleManager.addRole(new UserImpl("foo"));
+ role1.getProperties().put("key", "value1");
+ role1.getProperties().put("keyA", "valueA");
+ Role role2 = m_roleManager.addRole(new GroupImpl("bar"));
+ role2.getProperties().put("key", "value2");
+ role2.getProperties().put("keyB", "value1");
+
+ Filter filter;
+
+ filter = new FilterImpl("(key=value1)");
+ assertSameRoles(new Role[]{ role1 }, m_roleManager.getRoles(filter));
+
+ filter = new FilterImpl("(key=value2)");
+ assertSameRoles(new Role[]{ role2 }, m_roleManager.getRoles(filter));
+
+ filter = new FilterImpl("(key=value*)");
+ assertSameRoles(new Role[]{ role1, role2 }, m_roleManager.getRoles(filter));
+
+ filter = new FilterImpl("(|(key=value1)(keyB=value1))");
+ assertSameRoles(new Role[]{ role1, role2 }, m_roleManager.getRoles(filter));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#getRoles(String, String)}.
+ */
+ public void testGetRolesWithKeyValuePairOk() throws Exception {
+ Role role1 = m_roleManager.addRole(new UserImpl("foo"));
+ role1.getProperties().put("key", "value1");
+ role1.getProperties().put("keyA", "valueA");
+ Role role2 = m_roleManager.addRole(new GroupImpl("bar"));
+ role2.getProperties().put("key", "value2");
+ role2.getProperties().put("keyB", "value1");
+
+ assertSameRoles(new Role[]{ role1 }, m_roleManager.getRoles("key", "value1"));
+ assertSameRoles(new Role[]{ role2 }, m_roleManager.getRoles("key", "value2"));
+ assertSameRoles(new Role[0], m_roleManager.getRoles("key", "value"));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#getRoles(org.osgi.framework.Filter)}.
+ */
+ public void testGetRolesWithoutFilterOk() {
+ Role role1 = m_roleManager.addRole(new UserImpl("foo"));
+ Role role2 = m_roleManager.addRole(new GroupImpl("bar"));
+
+ assertSameRoles(new Role[]{ role2, role1 }, m_roleManager.getRoles(null));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#getRoleByName(java.lang.String)}.
+ */
+ public void testGetUserAnyoneRoleByName() {
+ assertSame(RoleRepository.USER_ANYONE, m_roleManager.getRoleByName(Role.USER_ANYONE));
+ }
+
+ /**
+ * Tests whether removing a role from a group causes an event to be emitted to the {@link RoleRepository}.
+ */
+ public void testRemoveBasicRoleYieldsEventOk() throws Exception {
+ final GroupImpl role = (GroupImpl) m_roleManager.addRole(new GroupImpl("foo"));
+ role.addMember(RoleRepository.USER_ANYONE);
+
+ m_latch = new CountDownLatch(1);
+
+ new Thread(new Runnable() {
+ public void run() {
+ role.removeMember(RoleRepository.USER_ANYONE);
+ };
+ }).start();
+
+ assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#removeRole(Role)}.
+ */
+ public void testRemoveExistingRoleOk() {
+ UserImpl role = new UserImpl("foo");
+ assertSame(role, m_roleManager.addRole(role));
+
+ assertTrue(m_roleManager.removeRole(role));
+ assertEquals(0, m_roleManager.getRoles(null).size());
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#removeRole(Role)}.
+ */
+ public void testRemoveNonExistingRoleOk() {
+ UserImpl role1 = new UserImpl("foo");
+ UserImpl role2 = new UserImpl("bar");
+ assertSame(role1, m_roleManager.addRole(role1));
+
+ assertFalse(m_roleManager.removeRole(role2));
+ assertEquals(1, m_roleManager.getRoles(null).size());
+ }
+
+ /**
+ * Tests that removing a null {@link RoleChangeListener} instance does not work.
+ */
+ public void testRemoveNullRoleChangeListenerFail() throws Exception {
+ try {
+ m_roleManager.removeRoleChangeListener(null);
+ fail("Expected IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected...
+ }
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#removeRole(Role)}.
+ */
+ public void testRemovePredefinedRoleFails() {
+ m_roleManager.addRole(new UserImpl("foo"));
+
+ Role role = RoleRepository.USER_ANYONE;
+ assertFalse(m_roleManager.removeRole(role));
+
+ assertEquals(1, m_roleManager.getRoles(null).size());
+ }
+
+ /**
+ * Tests whether removing a role from a group causes an event to be emitted to the {@link RoleRepository}.
+ */
+ public void testRemoveRequiredRoleYieldsEventOk() throws Exception {
+ final GroupImpl role = (GroupImpl) m_roleManager.addRole(new GroupImpl("foo"));
+ role.addRequiredMember(RoleRepository.USER_ANYONE);
+
+ m_latch = new CountDownLatch(1);
+
+ new Thread(new Runnable() {
+ public void run() {
+ role.removeMember(RoleRepository.USER_ANYONE);
+ };
+ }).start();
+
+ assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Tests that removing a valid {@link RoleChangeListener} instance works.
+ */
+ public void testRemoveRoleChangeListenerOk() throws Exception {
+ // Should succeed...
+ m_roleManager.removeRoleChangeListener(new RoleChangeListener() {
+ public void propertyAdded(Role role, Object key, Object value) {
+ }
+
+ public void propertyChanged(Role role, Object key, Object oldValue, Object newValue) {
+ }
+
+ public void propertyRemoved(Role role, Object key) {
+ }
+
+ public void roleAdded(Role role) {
+ }
+
+ public void roleRemoved(Role role) {
+ }
+ });
+ }
+
+ /**
+ * Tests whether removing a property from a role causes an event to be emitted to the {@link RoleRepository}.
+ */
+ public void testRemoveRolePropertyYieldsEventOk() throws Exception {
+ final Role role = m_roleManager.addRole(new UserImpl("john.doe"));
+ role.getProperties().put("key", "value");
+
+ m_latch = new CountDownLatch(1);
+
+ new Thread(new Runnable() {
+ public void run() {
+ role.getProperties().remove("key");
+ };
+ }).start();
+
+ assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * Tests that adding a role that does not inherit from RoleImpl does not work and yields an exception.
+ */
+ public void testRemoveRoleWithInvalidRoleFail() {
+ try {
+ m_roleManager.removeRole(new Role() {
+ public String getName() {
+ return "A User";
+ }
+ public Dictionary getProperties() {
+ return new Properties();
+ }
+ public int getType() {
+ return Role.USER;
+ }
+ });
+
+ fail("Exception expected!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that removing a null-role does not work and yields an exception.
+ */
+ public void testRemoveRoleWithNullRoleFail() {
+ try {
+ m_roleManager.removeRole(null);
+
+ fail("Exception expected!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests whether removing a credential from a user causes an event to be emitted to the {@link RoleRepository}.
+ */
+ public void testRemoveUserCredentialYieldsEventOk() throws Exception {
+ final UserImpl role = (UserImpl) m_roleManager.addRole(new UserImpl("john.doe"));
+ role.getCredentials().put("key", "value");
+
+ m_latch = new CountDownLatch(1);
+
+ new Thread(new Runnable() {
+ public void run() {
+ role.getCredentials().remove("key");
+ };
+ }).start();
+
+ assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_roleManager = new RoleRepository(new MemoryRoleRepositoryStore());
+ m_roleManager.addRoleChangeListener(new RoleChangeListener() {
+ public void propertyAdded(Role role, Object key, Object value) {
+ if (m_latch != null) {
+ m_latch.countDown();
+ }
+ }
+
+ public void propertyChanged(Role role, Object key, Object oldValue, Object newValue) {
+ if (m_latch != null) {
+ m_latch.countDown();
+ }
+ }
+
+ public void propertyRemoved(Role role, Object key) {
+ if (m_latch != null) {
+ m_latch.countDown();
+ }
+ }
+
+ public void roleAdded(Role role) {
+ if (m_latch != null) {
+ m_latch.countDown();
+ }
+ }
+
+ public void roleRemoved(Role role) {
+ if (m_latch != null) {
+ m_latch.countDown();
+ }
+ }
+ });
+
+ m_roleManager.start();
+ }
+
+ /**
+ * Asserts that a given collection of roles has the correct expected contents.
+ *
+ * @param expected
+ * @param roles
+ */
+ private void assertSameRoles(Role[] expected, Collection roles) {
+ assertTrue("Expected " + expected.length + " roles, got " + roles.size() + "!", expected.length == roles.size());
+
+ List e = new ArrayList(Arrays.asList(expected));
+ e.removeAll(roles);
+
+ assertTrue("Not seen: " + e, e.isEmpty());
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/UserAdminImplTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/UserAdminImplTest.java
new file mode 100644
index 0000000..4d895bf
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/UserAdminImplTest.java
@@ -0,0 +1,814 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl;
+
+import java.util.Arrays;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.framework.FilterImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.framework.Filter;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Test cases for {@link UserAdminImpl}.
+ */
+public class UserAdminImplTest extends TestCase {
+
+ /**
+ * Provides a stub implementation for {@link EventAdmin}.
+ */
+ static class StubEventAdmin implements EventAdmin {
+
+ public void postEvent(Event event) {
+ }
+
+ public void sendEvent(Event event) {
+ }
+ }
+
+ /**
+ * Provides a stub implementation for {@link UserAdminListenerList}.
+ */
+ static class StubUserAdminListenerList implements UserAdminListenerList {
+
+ public UserAdminListener[] getListeners() {
+ return new UserAdminListener[0];
+ }
+ }
+
+ private UserAdminImpl m_userAdmin;
+ private RoleRepository m_roleRepository;
+ private EventDispatcher m_dispatcher;
+
+ /**
+ * Tests that adding a basic member to a group works.
+ */
+ public void testAddGroupMemberOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+
+ assertTrue(group1.addMember(user1));
+ assertFalse(group1.addMember(user1));
+ }
+
+ /**
+ * Tests that adding a required member to a group works.
+ */
+ public void testAddRequiredGroupMemberOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+
+ assertTrue(group1.addRequiredMember(user1));
+ assertFalse(group1.addRequiredMember(user1));
+ }
+
+ /**
+ * Tests that adding a property of an invalid type to a role does not work and yields an exception.
+ */
+ public void testAddRolePropertyOfInvalidTypeFail() {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+
+ try {
+ user1.getProperties().put("key", Integer.valueOf(1));
+
+ fail("Expected IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that adding a property to a role works.
+ */
+ public void testAddRolePropertyOk() {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+
+ assertNull(user1.getProperties().get("key"));
+
+ user1.getProperties().put("key", "value");
+
+ assertEquals("value", user1.getProperties().get("key"));
+ }
+
+ /**
+ * Tests that adding a credential of an invalid type to a user does not work and yields an exception.
+ */
+ public void testAddUserCredentialInvalidTypeFails() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+
+ try {
+ user1.getCredentials().put("key", Integer.valueOf(1));
+
+ fail("Expected IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that adding a String credential to a user works.
+ */
+ public void testAddUserCredentialOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+
+ assertNull(user1.getCredentials().get("key"));
+
+ user1.getCredentials().put("key", "value");
+
+ assertEquals("value", user1.getCredentials().get("key"));
+
+ user1.getCredentials().put("key2", "value2".getBytes());
+
+ assertTrue(Arrays.equals("value2".getBytes(), (byte[]) user1.getCredentials().get("key2")));
+ }
+
+ /**
+ * Tests that testing for basic group membership works.
+ */
+ public void testBasicGroupMembershipOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ User user2 = (User) m_userAdmin.createRole("user2", Role.USER);
+ User user3 = (User) m_userAdmin.createRole("user3", Role.USER);
+
+ Group reqGroup = (Group) m_userAdmin.createRole("reqGroup", Role.GROUP);
+ reqGroup.addMember(user1);
+ reqGroup.addMember(user2);
+ reqGroup.addMember(user3);
+
+ Group group = (Group) m_userAdmin.createRole("group", Role.GROUP);
+ group.addRequiredMember(reqGroup);
+ group.addMember(user1);
+ group.addMember(user2);
+
+ Authorization auth = m_userAdmin.getAuthorization(user1);
+ assertTrue(auth.hasRole("group"));
+
+ auth = m_userAdmin.getAuthorization(user2);
+ assertTrue(auth.hasRole("group"));
+
+ auth = m_userAdmin.getAuthorization(user3);
+ assertFalse(auth.hasRole("group"));
+ }
+
+ /**
+ * Tests that changing a property to an invalid type does not work and yields an exception.
+ */
+ public void testChangeRolePropertyOfInvalidTypeFail() {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+ user1.getProperties().put("key", "value");
+
+ try {
+ user1.getProperties().put("key", Integer.valueOf(1));
+
+ fail("Expected IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that changing a property of a role works.
+ */
+ public void testChangeRolePropertyOk() {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+ user1.getProperties().put("key", "value");
+
+ assertEquals("value", user1.getProperties().get("key"));
+
+ user1.getProperties().put("key", "changed");
+
+ assertEquals("changed", user1.getProperties().get("key"));
+ }
+
+ /**
+ * Tests that changing a credential of a user works.
+ */
+ public void testChangeUserCredentialOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ user1.getCredentials().put("key", "value");
+
+ assertEquals("value", user1.getCredentials().get("key"));
+
+ user1.getCredentials().put("key", "changed");
+
+ assertEquals("changed", user1.getCredentials().get("key"));
+ }
+
+ /**
+ * Tests that creating an existing group does not succeed and yields null.
+ */
+ public void testCreateExistingGroupFail() {
+ Role role = null;
+
+ role = m_userAdmin.createRole("group1", Role.GROUP);
+ assertNotNull(role);
+
+ role = m_userAdmin.createRole("group1", Role.GROUP);
+ assertNull(role);
+ }
+
+ /**
+ * Tests that creating an existing role does not succeed and yields null.
+ */
+ public void testCreateExistingUserFail() {
+ Role role = null;
+
+ role = m_userAdmin.createRole("user1", Role.USER);
+ assertNotNull(role);
+
+ role = m_userAdmin.createRole("user1", Role.USER);
+ assertNull(role);
+ }
+
+ /**
+ * Tests that creating a role with an invalid type does not succeed and yields an exception.
+ */
+ public void testCreateInvalidRoleTypeFail() {
+ try {
+ m_userAdmin.createRole("role1", Role.ROLE);
+
+ fail("Expected an IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that creating a role without a name does not succeed and yields an exception.
+ */
+ public void testCreateInvalidRoleNameFail() {
+ try {
+ m_userAdmin.createRole(null, Role.USER);
+
+ fail("Expected an IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that creating a role without a name does not succeed and yields an exception.
+ */
+ public void testCreateRoleWithEmptyNameFail() {
+ try {
+ m_userAdmin.createRole("", Role.USER);
+
+ fail("Expected an IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that creating a non-existing group succeeds and yields a valid role instance.
+ */
+ public void testCreateNonExistingGroupOk() {
+ Role role = null;
+
+ role = m_userAdmin.createRole("group1", Role.GROUP);
+ assertNotNull(role);
+ assertEquals("group1", role.getName());
+ }
+
+ /**
+ * Tests that creating a non-existing role succeeds and yields a valid role instance.
+ */
+ public void testCreateNonExistingUserOk() {
+ Role role = null;
+
+ role = m_userAdmin.createRole("user1", Role.USER);
+ assertNotNull(role);
+ assertEquals("user1", role.getName());
+ }
+
+ /**
+ * Tests that creating a {@link UserAdminImpl} with a null event dispatcher fails.
+ */
+ public void testCreateUserAdminImplWithNullDispatcherFail() {
+ try {
+ new UserAdminImpl(m_roleRepository, null);
+
+ fail("Expected IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that creating a {@link UserAdminImpl} with a null role repository fails.
+ */
+ public void testCreateUserAdminImplWithNullRepositoryFail() {
+ try {
+ new UserAdminImpl(null, m_dispatcher);
+
+ fail("Expected IllegalArgumentException!");
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that obtaining the authorization for a non-existing user yields null.
+ */
+ public void testGetAuthorizationForAnonymousUserOk() {
+ Authorization auth = m_userAdmin.getAuthorization(null);
+
+ assertNotNull(auth);
+
+ assertNull(auth.getRoles());
+ assertNull(auth.getName());
+ }
+
+ /**
+ * Tests that obtaining the authorization for a non-existing user yields null.
+ */
+ public void testGetAuthorizationForNonExistingUserOk() {
+ Authorization auth = m_userAdmin.getAuthorization(new UserImpl("foo"));
+
+ assertNotNull(auth);
+
+ assertNull(auth.getRoles());
+ assertNotNull(auth.getName());
+ }
+
+ /**
+ * Tests that getting a existing group with an unique key-value pair does not work and yields null.
+ */
+ public void testGetExistingGroupFail() {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+ user1.getProperties().put("key1", "value1");
+ user1.getProperties().put("key2", "constant");
+ Role user2 = m_userAdmin.createRole("user2", Role.USER);
+ user2.getProperties().put("key1", "value2");
+ user1.getProperties().put("key2", "constant");
+ Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+ group1.getProperties().put("key1", "value3");
+
+ assertNull(m_userAdmin.getUser("key1", "value3"));
+ }
+
+ /**
+ * Tests that getting roles based on existing names works correctly.
+ */
+ public void testGetExistingRolesOk() {
+ m_userAdmin.createRole("user1", Role.USER);
+ m_userAdmin.createRole("user2", Role.USER);
+ m_userAdmin.createRole("user3", Role.USER);
+ m_userAdmin.createRole("group1", Role.GROUP);
+
+ assertEquals("user1", m_userAdmin.getRole("user1").getName());
+ assertEquals("user2", m_userAdmin.getRole("user2").getName());
+ assertEquals("user3", m_userAdmin.getRole("user3").getName());
+ assertEquals("group1", m_userAdmin.getRole("group1").getName());
+ }
+
+ /**
+ * Tests that getting an existing user with a non unique key-value pair does not work and yields null.
+ */
+ public void testGetExistingUserWithNonUniqueKeyValueFail() {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+ user1.getProperties().put("key1", "value1");
+ user1.getProperties().put("key2", "constant");
+ Role user2 = m_userAdmin.createRole("user2", Role.USER);
+ user2.getProperties().put("key1", "value2");
+ user2.getProperties().put("key2", "constant");
+ Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+ group1.getProperties().put("key1", "value3");
+
+ assertNull(m_userAdmin.getUser("key2", "constant"));
+ }
+
+ /**
+ * Tests that getting an existing user with an unique key-value pair works and yields the expected result.
+ */
+ public void testGetExistingUserWithUniqueKeyValueOk() {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+ user1.getProperties().put("key1", "value1");
+ user1.getProperties().put("key2", "constant");
+ Role user2 = m_userAdmin.createRole("user2", Role.USER);
+ user2.getProperties().put("key1", "value2");
+ user2.getProperties().put("key2", "constant");
+ Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+ group1.getProperties().put("key1", "value3");
+
+ assertEquals(user1, m_userAdmin.getUser("key1", "value1"));
+ assertEquals(user2, m_userAdmin.getUser("key1", "value2"));
+ }
+
+ /**
+ * Tests that retrieving the basic members from a group works.
+ */
+ public void testGetGroupMemberOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+
+ Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+ assertNull(group1.getMembers());
+
+ assertTrue(group1.addMember(user1));
+
+ assertEquals(1, group1.getMembers().length);
+ }
+
+ /**
+ * Tests that getting a non existing role by its name does not work and yields null.
+ */
+ public void testGetNonExistingRoleFails() {
+ assertNull(m_userAdmin.getRole("user1"));
+ }
+
+ /**
+ * Tests that getting a non-existing user does not work and yields null.
+ */
+ public void testGetNonExistingUserFail() {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+ user1.getProperties().put("key1", "value1");
+ user1.getProperties().put("key2", "constant");
+ Role user2 = m_userAdmin.createRole("user2", Role.USER);
+ user2.getProperties().put("key1", "value2");
+ user1.getProperties().put("key2", "constant");
+ Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+ group1.getProperties().put("key1", "value3");
+
+ assertNull(m_userAdmin.getUser("key1", "value4"));
+ }
+
+ /**
+ * Tests that getting roles based on a OSGi-filter that does not match any roles yields null.
+ */
+ public void testGetNonMatchingRolesOk() throws Exception {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+ user1.getProperties().put("key", "value1");
+ Role user2 = m_userAdmin.createRole("user2", Role.USER);
+ user2.getProperties().put("key", "value2");
+ Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+ group1.getProperties().put("key", "value3");
+
+ Role[] roles = m_userAdmin.getRoles("(nonExisting=value*)");
+ assertNull(roles);
+ }
+
+ /**
+ * Tests that getting predefined roles based on their names works correctly.
+ */
+ public void testGetPredefinedRolesOk() {
+ assertEquals(Role.USER_ANYONE, m_userAdmin.getRole(Role.USER_ANYONE).getName());
+ }
+
+ /**
+ * Tests that getting a removed role yields null.
+ */
+ public void testGetRemovedRoleFail() {
+ m_userAdmin.createRole("user1", Role.USER);
+ assertEquals("user1", m_userAdmin.getRole("user1").getName());
+
+ assertTrue(m_userAdmin.removeRole("user1"));
+ assertNull(m_userAdmin.getRole("user1"));
+ }
+
+ /**
+ * Tests that retrieving the required members from a group works.
+ */
+ public void testGetRequiredGroupMemberOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+
+ Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+ assertNull(group1.getRequiredMembers());
+
+ assertTrue(group1.addRequiredMember(user1));
+
+ assertEquals(1, group1.getRequiredMembers().length);
+ }
+
+ /**
+ * Tests that getting the various names of defined roles works and yields the expected result.
+ */
+ public void testGetRoleNamesOk() {
+ m_userAdmin.createRole("user1", Role.USER);
+ m_userAdmin.createRole("group1", Role.GROUP);
+
+ assertEquals(Role.USER_ANYONE, m_userAdmin.getRole(Role.USER_ANYONE).getName());
+ assertEquals("user1", m_userAdmin.getRole("user1").getName());
+ assertEquals("group1", m_userAdmin.getRole("group1").getName());
+ }
+
+ /**
+ * Tests that getting multiple roles based on a OSGi-filter works and yields the correct result.
+ */
+ public void testGetRolesWithFilterOk() throws Exception {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+ user1.getProperties().put("key", "value1");
+ Role user2 = m_userAdmin.createRole("user2", Role.USER);
+ user2.getProperties().put("key", "value2");
+ Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+ group1.getProperties().put("key", "value3");
+ Role group2 = m_userAdmin.createRole("group2", Role.GROUP);
+ group2.getProperties().put("key", "otherValue3");
+
+ Role[] roles = m_userAdmin.getRoles("(key=value*)");
+ assertNotNull(roles);
+
+ assertEquals(3, roles.length);
+
+ List roleList = Arrays.asList(roles);
+ assertTrue(roleList.contains(user1));
+ assertTrue(roleList.contains(user2));
+ assertTrue(roleList.contains(group1));
+ }
+
+ /**
+ * Tests that getting roles based on an invalid OSGi-filter yields an exception.
+ */
+ public void testGetRolesWithInvalidFilterFails() throws Exception {
+ try {
+ m_userAdmin.getRoles("(nonExisting=value*");
+
+ fail("Expected an InvalidSyntaxException!");
+ } catch (InvalidSyntaxException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that getting multiple roles based on a OSGi-filter works and yields the correct result.
+ */
+ public void testGetRolesWithoutFilterOk() throws Exception {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+ user1.getProperties().put("key", "value1");
+ Role user2 = m_userAdmin.createRole("user2", Role.USER);
+ user2.getProperties().put("key", "value2");
+ Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+ group1.getProperties().put("key", "value3");
+ Role group2 = m_userAdmin.createRole("group2", Role.GROUP);
+ group2.getProperties().put("key", "otherValue3");
+
+ Role[] roles = m_userAdmin.getRoles(null);
+ assertNotNull(roles);
+
+ assertEquals(4, roles.length);
+
+ List roleList = Arrays.asList(roles);
+ assertTrue(roleList.contains(user1));
+ assertTrue(roleList.contains(user2));
+ assertTrue(roleList.contains(group1));
+ assertTrue(roleList.contains(group2));
+ }
+
+ /**
+ * Tests that getting the various types of defined roles works and yields the expected result.
+ */
+ public void testGetRoleTypesOk() {
+ m_userAdmin.createRole("user1", Role.USER);
+ m_userAdmin.createRole("group1", Role.GROUP);
+
+ assertEquals(Role.ROLE, m_userAdmin.getRole(Role.USER_ANYONE).getType());
+ assertEquals(Role.USER, m_userAdmin.getRole("user1").getType());
+ assertEquals(Role.GROUP, m_userAdmin.getRole("group1").getType());
+ }
+
+ /**
+ * Tests that testing for group membership with anonymous users works.
+ */
+ public void testGroupMembershipWithAnonymousUserOk() {
+ Role user = m_userAdmin.createRole("user", Role.USER);
+
+ Group group = (Group) m_userAdmin.createRole("group", Role.GROUP);
+ group.addMember(user);
+
+ Authorization auth = m_userAdmin.getAuthorization(null);
+ assertTrue(auth.hasRole(Role.USER_ANYONE));
+ assertFalse(auth.hasRole("group"));
+ }
+
+ /**
+ * Tests that testing for group membership with "user.anyone" works.
+ */
+ public void testGroupMembershipWithUserAnyoneOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ User user2 = (User) m_userAdmin.createRole("user2", Role.USER);
+ User user3 = (User) m_userAdmin.createRole("user3", Role.USER);
+ User user4 = (User) m_userAdmin.createRole("user4", Role.USER);
+
+ Group reqGroup = (Group) m_userAdmin.createRole("reqGroup", Role.GROUP);
+ reqGroup.addMember(user1);
+ reqGroup.addMember(user2);
+
+ Group group = (Group) m_userAdmin.createRole("group", Role.GROUP);
+ group.addRequiredMember(reqGroup);
+ group.addMember(m_userAdmin.getRole(Role.USER_ANYONE));
+
+ Authorization auth = m_userAdmin.getAuthorization(user1);
+ assertTrue(auth.hasRole("group"));
+
+ auth = m_userAdmin.getAuthorization(user2);
+ assertTrue(auth.hasRole("group"));
+
+ auth = m_userAdmin.getAuthorization(user3);
+ assertFalse(auth.hasRole("group"));
+
+ auth = m_userAdmin.getAuthorization(user4);
+ assertFalse(auth.hasRole("group"));
+ }
+
+ /**
+ * Tests that testing the credentials for a user works and yields the correct result.
+ */
+ public void testHasUserCredentialOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ user1.getCredentials().put("key1", "value");
+ user1.getCredentials().put("key2", "value".getBytes());
+
+ assertTrue(user1.hasCredential("key1", "value"));
+ assertTrue(user1.hasCredential("key1", "value".getBytes()));
+ assertTrue(user1.hasCredential("key2", "value"));
+ assertTrue(user1.hasCredential("key2", "value".getBytes()));
+ assertFalse(user1.hasCredential("otherKey", "value"));
+ }
+
+ /**
+ * Tests that removing an existing role works.
+ */
+ public void testRemoveExistingRoleOk() {
+ Role role = null;
+
+ role = m_userAdmin.createRole("group1", Role.GROUP);
+ assertNotNull(role);
+
+ assertTrue(m_userAdmin.removeRole("group1"));
+ assertFalse(m_userAdmin.removeRole("group1"));
+ }
+
+ /**
+ * Tests that removing a non existing role does not work, yields a false result.
+ */
+ public void testRemoveNonExistingRoleOk() {
+ assertFalse(m_userAdmin.removeRole("group1"));
+ }
+
+ /**
+ * Tests that removing a predefined role does not work, and yields a false result.
+ */
+ public void testRemovePredefinedRoleOk() {
+ assertFalse(m_userAdmin.removeRole(Role.USER_ANYONE));
+ }
+
+ /**
+ * Tests that remove a property of a role works.
+ */
+ public void testRemoveRolePropertyOk() {
+ Role user1 = m_userAdmin.createRole("user1", Role.USER);
+ user1.getProperties().put("key", "value");
+
+ assertEquals("value", user1.getProperties().get("key"));
+
+ user1.getProperties().remove("key");
+
+ assertNull(user1.getProperties().get("key"));
+ }
+
+ /**
+ * Tests that remove a credential of a user works.
+ */
+ public void testRemoveUserCredentialOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ user1.getCredentials().put("key", "value");
+
+ assertEquals("value", user1.getCredentials().get("key"));
+
+ user1.getCredentials().remove("key");
+
+ assertNull(user1.getCredentials().get("key"));
+ }
+
+ /**
+ * Tests that removing a basic member from a group works.
+ */
+ public void testRemovingGroupMemberOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+
+ assertTrue(group1.addMember(user1));
+
+ assertTrue(group1.removeMember(user1));
+ assertFalse(group1.removeMember(user1));
+ }
+
+ /**
+ * Tests that removing a required member from a group works.
+ */
+ public void testRemovingRequiredGroupMemberOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+
+ assertTrue(group1.addRequiredMember(user1));
+
+ assertTrue(group1.removeMember(user1));
+ assertFalse(group1.removeMember(user1));
+ }
+
+ /**
+ * Tests that testing for required group membership works.
+ */
+ public void testRequiredGroupMembershipOk() {
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ User user2 = (User) m_userAdmin.createRole("user2", Role.USER);
+ User user3 = (User) m_userAdmin.createRole("user3", Role.USER);
+
+ Group reqGroup = (Group) m_userAdmin.createRole("reqGroup", Role.GROUP);
+ reqGroup.addMember(user1);
+ reqGroup.addMember(user2);
+ reqGroup.addMember(user3);
+
+ Group group = (Group) m_userAdmin.createRole("group", Role.GROUP);
+ group.addRequiredMember(reqGroup);
+
+ Authorization auth = m_userAdmin.getAuthorization(user1);
+ assertFalse(auth.hasRole("group"));
+
+ auth = m_userAdmin.getAuthorization(user2);
+ assertFalse(auth.hasRole("group"));
+
+ auth = m_userAdmin.getAuthorization(user3);
+ assertFalse(auth.hasRole("group"));
+ }
+
+ /**
+ * Tests that the list of roles in an {@link Authorization} does not contain the any-user, although it is defined as group member.
+ */
+ public void testUserAnyoneIsNotPartOfAuthorizedRolesOk() {
+ Role userAnyone = m_userAdmin.getRole(Role.USER_ANYONE);
+ User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+ Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+
+ assertTrue(group1.addRequiredMember(user1));
+ assertTrue(group1.addMember(userAnyone));
+
+ Authorization auth = m_userAdmin.getAuthorization(user1);
+ assertNotNull(auth);
+
+ assertTrue(auth.hasRole("group1"));
+
+ String[] roles = auth.getRoles();
+ assertNotNull(roles);
+
+ for (int i = 0; i < roles.length; i++) {
+ assertFalse(Role.USER_ANYONE.equals(roles[i]));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_roleRepository = new RoleRepository(new MemoryRoleRepositoryStore());
+ m_dispatcher = new EventDispatcher(new StubEventAdmin(), new StubUserAdminListenerList());
+
+ m_userAdmin = new UserAdminImpl(m_roleRepository, m_dispatcher) {
+ protected Filter createFilter(String filter) throws InvalidSyntaxException {
+ if (filter == null || "".equals(filter.trim())) {
+ return null;
+ }
+ return new FilterImpl(filter);
+ }
+ };
+
+ m_roleRepository.start();
+ m_dispatcher.start();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void tearDown() throws Exception {
+ m_roleRepository.stop();
+ m_dispatcher.stop();
+
+ super.tearDown();
+ }
+
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplSecurityTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplSecurityTest.java
new file mode 100644
index 0000000..652c3a7
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplSecurityTest.java
@@ -0,0 +1,160 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.impl.role;
+
+import java.security.Permission;
+
+import junit.framework.TestCase;
+
+
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Security-related test cases for {@link GroupImpl}.
+ */
+public class GroupImplSecurityTest extends TestCase {
+
+ private TestSecurityManager m_securityManager;
+ private GroupImpl m_group;
+
+ /**
+ * Tests that with permission, the {@link GroupImpl#addMember(Role)} method can be accessed.
+ */
+ public void testAddMemberWithPermissionsOk() throws SecurityException {
+ m_securityManager.m_allowed = true;
+
+ m_group.addMember(new RoleImpl(Role.USER_ANYONE));
+ }
+
+ /**
+ * Tests that without permission, the {@link GroupImpl#addMember(Role)} method can not be accessed.
+ */
+ public void testAddMemberWithoutPermissionsFail() throws SecurityException {
+ try {
+ m_group.addMember(new RoleImpl(Role.USER_ANYONE));
+
+ fail("Security exception expected!");
+ } catch (SecurityException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that with permission, the {@link GroupImpl#addRequiredMember(Role)} method can be accessed.
+ */
+ public void testAddRequiredMemberWithPermissionsOk() throws SecurityException {
+ m_securityManager.m_allowed = true;
+
+ m_group.addRequiredMember(new RoleImpl(Role.USER_ANYONE));
+ }
+
+ /**
+ * Tests that without permission, the {@link GroupImpl#addRequiredMember(Role)} method can not be accessed.
+ */
+ public void testAddRequiredMemberWithoutPermissionsFail() throws SecurityException {
+ try {
+ m_group.addRequiredMember(new RoleImpl(Role.USER_ANYONE));
+
+ fail("Security exception expected!");
+ } catch (SecurityException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that no special permissions are needed to access the {@link GroupImpl#getMembers()} method.
+ */
+ public void testGetMembers() throws SecurityException {
+ assertNull(m_group.getMembers());
+ }
+
+ /**
+ * Tests that no special permissions are needed to access the {@link GroupImpl#getRequiredMembers()} method.
+ */
+ public void testGetRequiredMembers() throws SecurityException {
+ assertNull(m_group.getRequiredMembers());
+ }
+
+ /**
+ * Tests that with permission, the {@link GroupImpl#removeMember(Role)} method can be accessed.
+ */
+ public void testRemoveMemberWithPermissionsOk() throws SecurityException {
+ m_securityManager.m_allowed = true;
+
+ assertFalse(m_group.removeMember(new RoleImpl(Role.USER_ANYONE)));
+ }
+
+ /**
+ * Tests that without permission, the {@link GroupImpl#removeMember(Role)} method can not be accessed.
+ */
+ public void testRemoveMemberWithoutPermissionsFail() throws SecurityException {
+ try {
+ assertFalse(m_group.removeMember(new RoleImpl(Role.USER_ANYONE)));
+
+ fail("Security exception expected!");
+ } catch (SecurityException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_securityManager = new TestSecurityManager();
+ System.setSecurityManager(m_securityManager);
+
+ m_group = new GroupImpl("group");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ System.setSecurityManager(null);
+ }
+
+ /**
+ * Provides a test security manager.
+ */
+ static final class TestSecurityManager extends SecurityManager {
+
+ volatile boolean m_allowed = false;
+
+ public void checkPermission(Permission perm) {
+ // Cannot use instanceof as it requires a special permission as well...
+ if ("UserAdminPermission".equals(perm.getClass().getSimpleName())) {
+ String name = perm.getName();
+ if ("admin".equals(name) && !m_allowed) {
+ throw new SecurityException("Not allowed!");
+ }
+ }
+ // Do not check for other permissions...
+ }
+
+ public void checkPermission(Permission perm, Object context) {
+ // Do not check for other permissions...
+ }
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplTest.java
new file mode 100644
index 0000000..14a2246
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplTest.java
@@ -0,0 +1,196 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl.role;
+
+
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Role;
+
+import junit.framework.TestCase;
+
+/**
+ * Test case for {@link GroupImpl}.
+ */
+public class GroupImplTest extends TestCase {
+
+ /**
+ * Tests that adding a role as basic member twice does not cause duplication.
+ */
+ public void testAddBasicMemberWithExistingBasicMemberOk() {
+ GroupImpl group = new GroupImpl("foo");
+ assertTrue(group.addMember(new UserImpl("bar")));
+ assertFalse(group.addMember(new UserImpl("bar"))); // should be ignored...
+
+ assertEquals(1, group.getMembers().length);
+ assertNull(group.getRequiredMembers());
+ }
+
+ /**
+ * Tests that adding a role as required member works if it is not contained at all.
+ */
+ public void testAddBasicMemberWithExistingRequiredMemberOk() {
+ GroupImpl group = new GroupImpl("foo");
+ assertTrue(group.addRequiredMember(new UserImpl("bar")));
+ assertFalse(group.addMember(new UserImpl("bar"))); // should be ignored...
+
+ assertNull(group.getMembers());
+ assertEquals(1, group.getRequiredMembers().length);
+ }
+
+ /**
+ * Tests that adding a role as basic member while another role with the same name exists does not cause duplication.
+ */
+ public void testAddBasicMemberWithExistingRoleOk() {
+ GroupImpl group = new GroupImpl("foo");
+ assertTrue(group.addMember(new UserImpl("bar")));
+ assertFalse(group.addMember(new RoleImpl("bar"))); // should be ignored...
+
+ assertEquals(1, group.getMembers().length);
+ assertNull(group.getRequiredMembers());
+ }
+
+ /**
+ * Tests that adding a role as basic member works if it is not contained at all.
+ */
+ public void testAddNonExistingMemberOk() {
+ GroupImpl group = new GroupImpl("foo");
+ assertTrue(group.addMember(new UserImpl("bar")));
+
+ assertEquals(1, group.getMembers().length);
+ assertNull(group.getRequiredMembers());
+ }
+
+ /**
+ * Tests that adding a role as basic member while it exists as required member does not cause duplication.
+ */
+ public void testAddNonExistingRequiredMemberOk() {
+ GroupImpl group = new GroupImpl("foo");
+ assertTrue(group.addRequiredMember(new UserImpl("bar")));
+
+ assertNull(group.getMembers());
+ assertEquals(1, group.getRequiredMembers().length);
+ }
+
+ /**
+ * Tests that adding a role as required member works if it is not contained at all.
+ */
+ public void testAddRequiredMemberWithExistingBasicMemberOk() {
+ GroupImpl group = new GroupImpl("foo");
+ assertTrue(group.addMember(new UserImpl("bar")));
+ assertFalse(group.addRequiredMember(new UserImpl("bar"))); // should be ignored...
+
+ assertEquals(1, group.getMembers().length);
+ assertNull(group.getRequiredMembers());
+ }
+
+ /**
+ * Tests that adding a role as required member while another role with the same name exists does not cause duplication.
+ */
+ public void testAddRequiredMemberWithExistingRoleOk() {
+ GroupImpl group = new GroupImpl("foo");
+ assertTrue(group.addRequiredMember(new UserImpl("bar")));
+ assertFalse(group.addRequiredMember(new RoleImpl("bar"))); // should be ignored...
+
+ assertNull(group.getMembers());
+ assertEquals(1, group.getRequiredMembers().length);
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.RoleImpl#getType()}.
+ */
+ public void testGetType() {
+ GroupImpl group = new GroupImpl("foo");
+
+ assertEquals(Role.GROUP, group.getType());
+ }
+
+ /**
+ * Tests that {@link GroupImpl#hashCode()} yields predictable results.
+ */
+ public void testHashCodeOk() {
+ GroupImpl group1 = new GroupImpl("foo");
+ GroupImpl group2 = new GroupImpl("foo");
+ GroupImpl group3 = new GroupImpl("bar");
+
+ assertTrue(group1.hashCode() == group2.hashCode());
+ assertFalse(group1.hashCode() == group3.hashCode());
+ assertFalse(group2.hashCode() == group3.hashCode());
+ }
+
+ /**
+ * Tests that removing an basic required member works.
+ */
+ public void testRemoveExistingBasicMemberOk() {
+ UserImpl role1 = new UserImpl("bar");
+ UserImpl role2 = new UserImpl("qux");
+
+ GroupImpl group = new GroupImpl("foo");
+ assertTrue(group.addMember(role1));
+ assertTrue(group.addRequiredMember(role2));
+
+ assertEquals(1, group.getMembers().length);
+ assertEquals(1, group.getRequiredMembers().length);
+
+ assertTrue(group.removeMember(role1));
+
+ assertNull(group.getMembers());
+ assertEquals(1, group.getRequiredMembers().length);
+ }
+
+ /**
+ * Tests that removing an existing required member works.
+ */
+ public void testRemoveExistingRequiredMemberOk() {
+ UserImpl role1 = new UserImpl("bar");
+ UserImpl role2 = new UserImpl("qux");
+
+ GroupImpl group = new GroupImpl("foo");
+ assertTrue(group.addMember(role1));
+ assertTrue(group.addRequiredMember(role2));
+
+ assertEquals(1, group.getMembers().length);
+ assertEquals(1, group.getRequiredMembers().length);
+
+ assertTrue(group.removeMember(role2));
+
+ assertEquals(1, group.getMembers().length);
+ assertNull(group.getRequiredMembers());
+ }
+
+ /**
+ * Tests that removing an basic required member works.
+ */
+ public void testRemoveNonExistingMemberOk() {
+ UserImpl role1 = new UserImpl("bar");
+ UserImpl role2 = new UserImpl("qux");
+ UserImpl role3 = new UserImpl("quu");
+
+ GroupImpl group = new GroupImpl("foo");
+ assertTrue(group.addMember(role1));
+ assertTrue(group.addRequiredMember(role2));
+
+ assertEquals(1, group.getMembers().length);
+ assertEquals(1, group.getRequiredMembers().length);
+
+ assertFalse(group.removeMember(role3));
+
+ assertEquals(1, group.getMembers().length);
+ assertEquals(1, group.getRequiredMembers().length);
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionarySecurityTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionarySecurityTest.java
new file mode 100644
index 0000000..200bbe4
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionarySecurityTest.java
@@ -0,0 +1,173 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.impl.role;
+
+import java.security.Permission;
+import java.util.Enumeration;
+
+
+import org.apache.felix.useradmin.impl.role.ObservableDictionary;
+import org.osgi.service.useradmin.UserAdminPermission;
+
+import junit.framework.TestCase;
+
+/**
+ * Security-related test cases for {@link ObservableDictionary}.
+ */
+public class ObservableDictionarySecurityTest extends TestCase {
+
+ private static final String GET_ACTION = UserAdminPermission.GET_CREDENTIAL;
+ private static final String CHANGE_ACTION = UserAdminPermission.CHANGE_CREDENTIAL;
+
+ private TestSecurityManager m_securityManager;
+ private ObservableDictionary m_dict;
+
+ /**
+ * Tests that no special permissions are needed to access the {@link ObservableDictionary#size()} method.
+ */
+ public void testSize() throws SecurityException {
+ assertEquals(0, m_dict.size());
+ }
+
+ /**
+ * Tests that no special permissions are needed to access the {@link ObservableDictionary#isEmpty()} method.
+ */
+ public void testIsEmpty() throws SecurityException {
+ assertTrue(m_dict.isEmpty());
+ }
+
+ /**
+ * Tests that no special permissions are needed to access the {@link ObservableDictionary#elements()} method.
+ */
+ public void testElements() throws SecurityException {
+ Enumeration elements = m_dict.elements();
+ assertNotNull(elements);
+ assertFalse(elements.hasMoreElements());
+ }
+
+ /**
+ * Tests that with permission, the {@link ObservableDictionary#get(Object)} method can be accessed.
+ */
+ public void testGetObjectWithPermissionsOk() throws SecurityException {
+ assertNull(m_dict.get("permFoo"));
+ }
+
+ /**
+ * Tests that without permission, the {@link ObservableDictionary#get(Object)} method can not be accessed.
+ */
+ public void testGetObjectWithoutPermissionsFail() throws SecurityException {
+ try {
+ assertNull(m_dict.get("bar"));
+
+ fail("Security exception expected!");
+ } catch (SecurityException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that no special permissions are needed to access the {@link ObservableDictionary#keys()} method.
+ */
+ public void testKeys() throws SecurityException {
+ Enumeration keys = m_dict.keys();
+ assertNotNull(keys);
+ assertFalse(keys.hasMoreElements());
+ }
+
+ /**
+ * Tests that with permission, the {@link ObservableDictionary#put(Object, Object)} method can be accessed.
+ */
+ public void testPutObjectWithPermissionsOk() throws SecurityException {
+ assertNull(m_dict.put("permKey", "value"));
+ }
+
+ /**
+ * Tests that without permission, the {@link ObservableDictionary#put(Object, Object)} method can not be accessed.
+ */
+ public void testPutObjectWithoutPermissionsFail() throws SecurityException {
+ try {
+ assertNull(m_dict.put("key", "value"));
+
+ fail("Security exception expected!");
+ } catch (SecurityException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that with permission, the {@link ObservableDictionary#remove(Object)} method can be accessed.
+ */
+ public void testRemoveObjectWithPermissionsOk() throws SecurityException {
+ assertNull(m_dict.remove("permKey"));
+ }
+
+ /**
+ * Tests that without permission, the {@link ObservableDictionary#remove(Object)} method can not be accessed.
+ */
+ public void testRemoveObjectWithoutPermissionsFail() throws SecurityException {
+ try {
+ assertNull(m_dict.remove("key"));
+
+ fail("Security exception expected!");
+ } catch (SecurityException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_securityManager = new TestSecurityManager();
+ System.setSecurityManager(m_securityManager);
+
+ m_dict = new ObservableDictionary(GET_ACTION, CHANGE_ACTION);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void tearDown() throws Exception {
+ super.tearDown();
+
+ System.setSecurityManager(null);
+ }
+
+ /**
+ * Provides a test security manager.
+ */
+ static final class TestSecurityManager extends SecurityManager {
+
+ public void checkPermission(Permission perm) {
+ // Cannot use instanceof as it requires a special permission as well...
+ if ("UserAdminPermission".equals(perm.getClass().getSimpleName())) {
+ String name = perm.getName();
+ if ((name != null) && !name.startsWith("perm")) {
+ throw new SecurityException("Not allowed!");
+ }
+ }
+ // Do not check for other permissions...
+ }
+
+ public void checkPermission(Permission perm, Object context) {
+ // Do not check for other permissions...
+ }
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionaryTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionaryTest.java
new file mode 100644
index 0000000..497d6a5
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionaryTest.java
@@ -0,0 +1,247 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl.role;
+
+import java.util.Date;
+
+import org.apache.felix.useradmin.impl.role.ObservableDictionary;
+import org.apache.felix.useradmin.impl.role.ObservableDictionary.DictionaryChangeListener;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Test case for {@link ObservableDictionary}.
+ */
+public class ObservableDictionaryTest extends TestCase {
+
+ /**
+ * Implementation of {@link DictionaryChangeListener} that keeps track of
+ * which callback methods are being called at least once.
+ */
+ static class StateCheckingDictionaryChangeListener implements DictionaryChangeListener {
+
+ volatile boolean m_addedCalled = false;
+ volatile boolean m_removedCalled = false;
+ volatile boolean m_changedCalled = false;
+
+ public void entryAdded(Object key, Object value) {
+ m_addedCalled = true;
+ }
+
+ public void entryChanged(Object key, Object oldValue, Object newValue) {
+ m_changedCalled = true;
+ }
+
+ public void entryRemoved(Object key) {
+ m_removedCalled = true;
+ }
+ }
+
+ private ObservableDictionary m_dict;
+ private StateCheckingDictionaryChangeListener m_listener;
+
+ /**
+ * Tests that adding an entry emits a change event.
+ */
+ public void testAddEntryEmitsEvent() {
+ m_dict.setDictionaryChangeListener(m_listener);
+
+ m_dict.put("key", "value");
+
+ assertTrue(m_listener.m_addedCalled);
+ assertFalse(m_listener.m_changedCalled);
+ assertFalse(m_listener.m_removedCalled);
+ }
+
+ /**
+ * Tests that changing an entry emits a change event.
+ */
+ public void testChangeEntryEmitsEvent() {
+ m_dict.put("key", "value1");
+
+ m_dict.setDictionaryChangeListener(m_listener);
+
+ m_dict.put("key", "value2");
+
+ assertTrue(m_listener.m_changedCalled);
+ assertFalse(m_listener.m_addedCalled);
+ assertFalse(m_listener.m_removedCalled);
+ }
+
+ /**
+ * Tests that creating a new {@link ObservableDictionary} with a valid dictionary succeeds.
+ */
+ public void testCreateWithNonNullDictionaryFail() {
+ m_dict.put("foo", "bar");
+ m_dict.put("bar", "foo");
+
+ m_dict = new ObservableDictionary("foo", "bar", m_dict);
+
+ assertEquals("bar", m_dict.get("foo"));
+ assertEquals("foo", m_dict.get("bar"));
+ }
+
+ /**
+ * Tests that creating a new {@link ObservableDictionary} with a <code>null</code> dictionary fails.
+ */
+ public void testCreateWithNullDictionaryFail() {
+ try {
+ new ObservableDictionary("foo", "bar", null);
+ fail("Expected IllegalArgumentException!");
+ } catch (Exception e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that we can get any kind of object from an {@link ObservableDictionary}.
+ */
+ public void testGetAnyKindOfObject() {
+ assertNull(m_dict.get("non-existing"));
+
+ Integer key = Integer.valueOf(3);
+ Date value = new Date();
+
+ m_dict.put(key, value);
+ assertEquals(value, m_dict.get(key));
+ }
+
+ /**
+ * Tests that we cannot get a null-key form a {@link ObservableDictionary}.
+ */
+ public void testGetNullKeyFail() {
+ try {
+ m_dict.get(null);
+
+ fail();
+ } catch (Exception e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Test that we can put any kind of object in a {@link ObservableDictionary}.
+ */
+ public void testPutAnyKindOfObject() {
+ Integer key = Integer.valueOf(3);
+ Date value = new Date();
+
+ m_dict.put(key, value);
+ assertEquals(value, m_dict.get(key));
+
+ m_dict.put(key, "other-value");
+ assertEquals("other-value", m_dict.get(key));
+ }
+
+ /**
+ * Test that we cannot put a null-key into a {@link ObservableDictionary}.
+ */
+ public void testPutNullKeyFail() {
+ try {
+ m_dict.put(null, "value");
+
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Test that we cannot put a null-value into a {@link ObservableDictionary}.
+ */
+ public void testPutNullValueFail() {
+ try {
+ m_dict.put("key", null);
+
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that removing a key emits a change event.
+ */
+ public void testRemoveEmitsEvent() {
+ m_dict.setDictionaryChangeListener(m_listener);
+
+ m_dict.remove("key");
+
+ assertTrue(m_listener.m_removedCalled);
+ assertFalse(m_listener.m_addedCalled);
+ assertFalse(m_listener.m_changedCalled);
+ }
+
+ /**
+ * Tests that we cannot remove a null-key from a {@link ObservableDictionary}.
+ */
+ public void testRemoveNullKeyFail() {
+ try {
+ m_dict.remove(null);
+
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that we can remove any kind of object from a {@link ObservableDictionary}.
+ */
+ public void testRemoveObject() {
+ Integer key = Integer.valueOf(3);
+ Date value = new Date();
+
+ m_dict.put(key, value);
+ assertEquals(value, m_dict.get(key));
+
+ m_dict.remove(key);
+ assertNull(m_dict.get(key));
+ }
+
+ /**
+ * Tests that {@link ObservableDictionary#equals(Object)} and {@link ObservableDictionary#hashCode()} work correctly.
+ */
+ public void testEqualsAndHashcode() {
+ ObservableDictionary d1 = new ObservableDictionary(null, null);
+ ObservableDictionary d2 = new ObservableDictionary(null, null);
+
+ assertTrue(d1.hashCode() == d2.hashCode());
+ assertTrue(d1.equals(d2));
+ assertTrue(d1.equals(d1));
+
+ d2.put("foo", "bar");
+
+ assertFalse(d1.hashCode() == d2.hashCode());
+ assertFalse(d1.equals(d2));
+ assertFalse(d1.equals(null));
+ assertFalse(d1.equals("bar"));
+ assertTrue(d1.equals(d1));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_dict = new ObservableDictionary(null, null);
+ m_listener = new StateCheckingDictionaryChangeListener();
+
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservablePropertiesTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservablePropertiesTest.java
new file mode 100644
index 0000000..122c329
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservablePropertiesTest.java
@@ -0,0 +1,119 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.impl.role;
+
+import org.apache.felix.useradmin.impl.role.ObservableProperties;
+
+import junit.framework.TestCase;
+
+/**
+ * Test case for {@link ObservableProperties}.
+ */
+public class ObservablePropertiesTest extends TestCase {
+
+ private ObservableProperties m_dict;
+
+ /**
+ * Tests that calling get with a non-string value yields an exception.
+ */
+ public void testGetNonStringKeyFail() {
+ try {
+ m_dict.get(Integer.valueOf(1));
+
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that calling get with a string value does not yield an exception.
+ */
+ public void testGetStringKeyOk() {
+ assertNull(m_dict.get("key"));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#put(java.lang.Object, java.lang.Object)}.
+ */
+ public void testPutByteArrayValueOk() {
+ assertNull(m_dict.put("key", "value".getBytes()));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#put(java.lang.Object, java.lang.Object)}.
+ */
+ public void testPutNonStringKeyFail() {
+ try {
+ m_dict.put(Integer.valueOf(1), "value");
+
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#put(java.lang.Object, java.lang.Object)}.
+ */
+ public void testPutNonStringValueFail() {
+ try {
+ m_dict.put("key", Integer.valueOf(1));
+
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#put(java.lang.Object, java.lang.Object)}.
+ */
+ public void testPutStringKeyValueOk() {
+ assertNull(m_dict.put("key", "value"));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#remove(java.lang.Object)}.
+ */
+ public void testRemoveNonStringKeyFail() {
+ try {
+ m_dict.remove(Integer.valueOf(1));
+
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#remove(java.lang.Object)}.
+ */
+ public void testRemoveStringKeyOk() {
+ assertNull(m_dict.remove("foo"));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ m_dict = new ObservableProperties(null, null);
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/RoleImplTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/RoleImplTest.java
new file mode 100644
index 0000000..a393d13
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/RoleImplTest.java
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.useradmin.impl.role;
+
+import java.util.Dictionary;
+
+import junit.framework.TestCase;
+
+
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Test case for {@link RoleImpl}.
+ */
+public class RoleImplTest extends TestCase {
+
+ /**
+ * Tests that we must enter a valid name upon constructing a {@link RoleImpl}.
+ */
+ public void testCreateNullNameFail() {
+ try {
+ new RoleImpl(null);
+
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that we must enter a valid name upon constructing a {@link RoleImpl}.
+ */
+ public void testCreateEmptyNameFail() {
+ try {
+ new RoleImpl(" ");
+
+ fail();
+ } catch (IllegalArgumentException e) {
+ // Ok; expected
+ }
+ }
+
+ /**
+ * Tests that we can obtain the name of a {@link RoleImpl} as set in its constructor.
+ */
+ public void testGetName() {
+ RoleImpl role = new RoleImpl("foo");
+ assertEquals("foo", role.getName());
+ }
+
+ /**
+ * Tests that we can obtain the properties of a {@link RoleImpl}.
+ */
+ public void testGetProperties() {
+ RoleImpl role = new RoleImpl("foo");
+
+ Dictionary dict = role.getProperties();
+ assertNotNull(dict);
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.RoleImpl#getType()}.
+ */
+ public void testGetType() {
+ RoleImpl role = new RoleImpl("foo");
+ assertEquals(Role.ROLE, role.getType());
+ }
+
+ /**
+ * Tests that {@link RoleImpl#hashCode()} yields predictable results.
+ */
+ public void testHashCodeOk() {
+ RoleImpl role1 = new RoleImpl("foo");
+ RoleImpl role2 = new RoleImpl("foo");
+ RoleImpl role3 = new RoleImpl("bar");
+
+ assertTrue(role1.hashCode() == role2.hashCode());
+ assertFalse(role1.hashCode() == role3.hashCode());
+ assertFalse(role2.hashCode() == role3.hashCode());
+ }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/UserImplTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/UserImplTest.java
new file mode 100644
index 0000000..4a308b5
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/UserImplTest.java
@@ -0,0 +1,100 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.felix.useradmin.impl.role;
+
+import java.util.Dictionary;
+
+
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Role;
+
+import junit.framework.TestCase;
+
+/**
+ * Test case for {@link UserImpl}.
+ */
+public class UserImplTest extends TestCase {
+
+ /**
+ * Tests that we can get the credentials for a {@link UserImpl}.
+ */
+ public void testGetCredentials() {
+ UserImpl user = new UserImpl("foo");
+
+ Dictionary dict = user.getCredentials();
+ assertNotNull(dict);
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.RoleImpl#getType()}.
+ */
+ public void testGetType() {
+ UserImpl user = new UserImpl("foo");
+
+ assertEquals(Role.USER, user.getType());
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.UserImpl#hasCredential(java.lang.String, java.lang.Object)}.
+ */
+ public void testHasExistingCredentialAlternativeValueTypeOk() {
+ UserImpl user = new UserImpl("foo");
+
+ Dictionary dict = user.getCredentials();
+ dict.put("password", "secret");
+
+ // Direct comparison...
+ assertTrue(user.hasCredential("password", "secret"));
+
+ // In case the given value is a byte[]...
+ assertTrue(user.hasCredential("password", "secret".getBytes()));
+
+ dict.put("password", "otherSecret".getBytes());
+
+ // Direct comparison...
+ assertTrue(user.hasCredential("password", "otherSecret".getBytes()));
+
+ // In case the stored value is a byte[]...
+ assertTrue(user.hasCredential("password", "otherSecret"));
+ }
+
+ /**
+ * Test method for {@link org.apache.felix.useradmin.impl.role.UserImpl#hasCredential(java.lang.String, java.lang.Object)}.
+ */
+ public void testHasExistingCredentialOk() {
+ UserImpl user = new UserImpl("foo");
+
+ Dictionary dict = user.getCredentials();
+ dict.put("password", "secret");
+
+ assertTrue(user.hasCredential("password", "secret"));
+ }
+
+ /**
+ * Tests that {@link UserImpl#hashCode()} yields predictable results.
+ */
+ public void testHashCodeOk() {
+ UserImpl user1 = new UserImpl("foo");
+ UserImpl user2 = new UserImpl("foo");
+ UserImpl user3 = new UserImpl("bar");
+
+ assertTrue(user1.hashCode() == user2.hashCode());
+ assertFalse(user1.hashCode() == user3.hashCode());
+ assertFalse(user2.hashCode() == user3.hashCode());
+ }
+}