+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<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.argeo.commons</groupId>
- <artifactId>argeo-commons</artifactId>
- <version>2.3-SNAPSHOT</version>
- <relativePath>..</relativePath>
- </parent>
- <artifactId>demo</artifactId>
- <name>Commons Demo</name>
- <packaging>pom</packaging>
- <build>
- </build>
- <dependencies>
- <dependency>
- <groupId>org.argeo.tp.equinox</groupId>
- <artifactId>org.eclipse.osgi</artifactId>
- </dependency>
- <dependency>
- <groupId>org.argeo.commons</groupId>
- <artifactId>org.argeo.osgi.boot</artifactId>
- <version>2.3-SNAPSHOT</version>
- </dependency>
- </dependencies>
- <profiles>
- <profile>
- <id>argeo_node_rap</id>
- <build>
- <plugins>
- <plugin>
- <groupId>org.argeo.maven.plugins</groupId>
- <artifactId>argeo-osgi-plugin</artifactId>
- <configuration>
- <systemPropertiesFile>argeo_node_rap.properties</systemPropertiesFile>
- <execDir>exec/argeo_node_rap</execDir>
- </configuration>
- </plugin>
- </plugins>
- </build>
- <dependencies>
- <dependency>
- <groupId>org.argeo.commons</groupId>
- <artifactId>org.argeo.dep.cms.e4.rap</artifactId>
- <version>2.3-SNAPSHOT</version>
- </dependency>
- </dependencies>
- </profile>
- <profile>
- <id>cms-e4-rap</id>
- <build>
- <plugins>
- <plugin>
- <groupId>org.argeo.maven.plugins</groupId>
- <artifactId>argeo-osgi-plugin</artifactId>
- <configuration>
- <systemPropertiesFile>cms-e4-rap.properties</systemPropertiesFile>
- <execDir>exec/cms-e4-rap</execDir>
- </configuration>
- </plugin>
- </plugins>
- </build>
- <dependencies>
- <dependency>
- <groupId>org.argeo.commons</groupId>
- <artifactId>org.argeo.dep.cms.e4.rap</artifactId>
- <version>2.3-SNAPSHOT</version>
- </dependency>
- </dependencies>
- </profile>
- </profiles>
-</project>
\ No newline at end of file
</dependency>
<dependency>
<groupId>org.argeo.commons</groupId>
- <artifactId>org.argeo.enterprise</artifactId>
+ <artifactId>org.argeo.util</artifactId>
<version>2.3-SNAPSHOT</version>
</dependency>
<!-- <dependency> -->
--- /dev/null
+<assembly
+ xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+ <id>dist</id>
+ <baseDirectory></baseDirectory>
+ <formats>
+ <format>zip</format>
+ </formats>
+ <fileSets>
+ <fileSet>
+ <directory>base/bin</directory>
+ <outputDirectory>bin</outputDirectory>
+ <fileMode>0755</fileMode>
+ <includes>
+ <include>*</include>
+ </includes>
+ <excludes>
+ <exclude>offline.sh</exclude>
+ </excludes>
+ </fileSet>
+ </fileSets>
+ <dependencySets>
+ <dependencySet>
+ <unpack>false</unpack>
+ <outputFileNameMapping>${artifact.artifactId}.${artifact.extension}</outputFileNameMapping>
+ <outputDirectory>share/osgi/boot</outputDirectory>
+ <includes>
+ <include>org.argeo.tp.equinox:org.eclipse.osgi</include>
+ <include>org.argeo.commons:org.argeo.osgi.boot</include>
+ </includes>
+ </dependencySet>
+ </dependencySets>
+</assembly>
\ No newline at end of file
--- /dev/null
+#!/bin/sh
+BIN_DIR=`dirname $0`
+EQUINOX=$BIN_DIR/../share/osgi/boot/org.eclipse.osgi.jar
+OSGI_BOOT=$BIN_DIR/../share/osgi/boot/org.argeo.osgi.boot.jar
+
+/usr/bin/jshell --class-path "$EQUINOX:$OSGI_BOOT" $*
--- /dev/null
+<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.argeo.commons</groupId>
+ <version>2.3-SNAPSHOT</version>
+ <artifactId>dist</artifactId>
+ <relativePath>..</relativePath>
+ </parent>
+ <artifactId>osgi-boot</artifactId>
+ <packaging>pom</packaging>
+ <name>Commons Deployable OSGi Boot</name>
+ <!-- <properties> -->
+ <!-- <version.equinox>3.10.1.v20140909-1633</version.equinox> -->
+ <!-- </properties> -->
+ <dependencies>
+ <dependency>
+ <groupId>org.argeo.tp</groupId>
+ <artifactId>argeo-tp</artifactId>
+ <version>${version.argeo-tp}</version>
+ </dependency>
+
+ <!-- OSGi Boot (and Equinox) -->
+ <dependency>
+ <groupId>org.argeo.commons</groupId>
+ <artifactId>org.argeo.init</artifactId>
+ <version>2.3-SNAPSHOT</version>
+ </dependency>
+ </dependencies>
+ <profiles>
+ <profile>
+ <id>dist</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <finalName>osgi-boot-${version.released}</finalName>
+ <appendAssemblyId>false</appendAssemblyId>
+ <descriptors>
+ <descriptor>assembly/osgi-boot.xml</descriptor>
+ </descriptors>
+ </configuration>
+ <executions>
+ <execution>
+ <id>assembly-base</id>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>rpmbuild</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>rpm-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>rpm-osgi-boot</id>
+ <phase>package</phase>
+ <goals>
+ <goal>rpm</goal>
+ </goals>
+ <configuration>
+ <name>argeo-init${argeo.rpm.suffix}</name>
+ <mappings>
+ <mapping>
+ <directory>/usr/bin</directory>
+ <username>root</username>
+ <groupname>root</groupname>
+ <filemode>755</filemode>
+ <directoryIncluded>false</directoryIncluded>
+ <sources>
+ <source>
+ <location>rpm/usr/bin</location>
+ <includes>
+ <include>*</include>
+ </includes>
+ </source>
+ </sources>
+ </mapping>
+ <mapping>
+ <directory>/usr/share/osgi/boot</directory>
+ <username>root</username>
+ <groupname>root</groupname>
+ <filemode>644</filemode>
+ <directoryIncluded>false</directoryIncluded>
+ <dependency>
+ <stripVersion>true</stripVersion>
+ <includes>
+ <include>org.argeo.commons:org.argeo.osgi.boot</include>
+ </includes>
+ </dependency>
+ </mapping>
+ <mapping>
+ <directory>/usr/share/osgi/boot</directory>
+ <username>root</username>
+ <groupname>root</groupname>
+ <filemode>644</filemode>
+ <configuration>noreplace</configuration>
+ <directoryIncluded>false</directoryIncluded>
+ <sources>
+ <source>
+ <location>rpm/usr/share/osgi/boot</location>
+ <includes>
+ <include>*.args</include>
+ </includes>
+ </source>
+ </sources>
+ </mapping>
+ </mappings>
+ <requires>
+ <require>argeo-init-tp-equinox${argeo.rpm.suffix}</require>
+ </requires>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ <profile>
+ <id>rpmbuild-tp</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>rpm-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>rpm-osgi-boot-equinox</id>
+ <phase>package</phase>
+ <goals>
+ <goal>rpm</goal>
+ </goals>
+ <configuration>
+ <name>argeo-init-tp-equinox${argeo.rpm.suffix}</name>
+ <projversion>${version.argeo-tp}</projversion>
+ <release>${argeo.rpm.release.tp}</release>
+ <mappings>
+ <mapping>
+ <directory>/usr/share/osgi/boot</directory>
+ <username>root</username>
+ <groupname>root</groupname>
+ <filemode>644</filemode>
+ <directoryIncluded>false</directoryIncluded>
+ <dependency>
+ <stripVersion>true</stripVersion>
+ <includes>
+ <include>org.argeo.tp.equinox:org.eclipse.osgi</include>
+ </includes>
+ </dependency>
+ </mapping>
+ </mappings>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
--- /dev/null
+#!/bin/sh
+
+export A2_HOME=$HOME/.a2
+if [ -d "$A2_HOME/share/osgi/boot" ]; then
+ PREFIX=$A2_HOME
+else
+ PREFIX=/usr
+fi
+
+EQUINOX=$PREFIX/share/osgi/boot/org.eclipse.osgi.jar
+OSGI_BOOT=$PREFIX/share/osgi/boot/org.argeo.osgi.boot.jar
+
+/usr/bin/jshell --class-path "$EQUINOX:$OSGI_BOOT" $*
--- /dev/null
+-jar /usr/share/osgi/boot/org.eclipse.osgi.jar
\ No newline at end of file
</mappings>
<requires>
<require>argeo-cms-node${argeo.rpm.suffix}</require>
- <require>argeo-osgi-boot${argeo.rpm.suffix}</require>
+ <require>argeo-init${argeo.rpm.suffix}</require>
<!-- do not explicitely require java -->
</requires>
</configuration>
+++ /dev/null
-<assembly
- xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
- <id>dist</id>
- <baseDirectory></baseDirectory>
- <formats>
- <format>zip</format>
- </formats>
- <fileSets>
- <fileSet>
- <directory>base/bin</directory>
- <outputDirectory>bin</outputDirectory>
- <fileMode>0755</fileMode>
- <includes>
- <include>*</include>
- </includes>
- <excludes>
- <exclude>offline.sh</exclude>
- </excludes>
- </fileSet>
- </fileSets>
- <dependencySets>
- <dependencySet>
- <unpack>false</unpack>
- <outputFileNameMapping>${artifact.artifactId}.${artifact.extension}</outputFileNameMapping>
- <outputDirectory>share/osgi/boot</outputDirectory>
- <includes>
- <include>org.argeo.tp.equinox:org.eclipse.osgi</include>
- <include>org.argeo.commons:org.argeo.osgi.boot</include>
- </includes>
- </dependencySet>
- </dependencySets>
-</assembly>
\ No newline at end of file
+++ /dev/null
-#!/bin/sh
-BIN_DIR=`dirname $0`
-EQUINOX=$BIN_DIR/../share/osgi/boot/org.eclipse.osgi.jar
-OSGI_BOOT=$BIN_DIR/../share/osgi/boot/org.argeo.osgi.boot.jar
-
-/usr/bin/jshell --class-path "$EQUINOX:$OSGI_BOOT" $*
+++ /dev/null
-<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.argeo.commons</groupId>
- <version>2.3-SNAPSHOT</version>
- <artifactId>dist</artifactId>
- <relativePath>..</relativePath>
- </parent>
- <artifactId>osgi-boot</artifactId>
- <packaging>pom</packaging>
- <name>Commons Deployable OSGi Boot</name>
- <!-- <properties> -->
- <!-- <version.equinox>3.10.1.v20140909-1633</version.equinox> -->
- <!-- </properties> -->
- <dependencies>
- <dependency>
- <groupId>org.argeo.tp</groupId>
- <artifactId>argeo-tp</artifactId>
- <version>${version.argeo-tp}</version>
- </dependency>
-
- <!-- OSGi Boot (and Equinox) -->
- <dependency>
- <groupId>org.argeo.commons</groupId>
- <artifactId>org.argeo.osgi.boot</artifactId>
- <version>2.3-SNAPSHOT</version>
- </dependency>
- </dependencies>
- <profiles>
- <profile>
- <id>dist</id>
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-assembly-plugin</artifactId>
- <configuration>
- <finalName>osgi-boot-${version.released}</finalName>
- <appendAssemblyId>false</appendAssemblyId>
- <descriptors>
- <descriptor>assembly/osgi-boot.xml</descriptor>
- </descriptors>
- </configuration>
- <executions>
- <execution>
- <id>assembly-base</id>
- <phase>package</phase>
- <goals>
- <goal>single</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
- </profile>
- <profile>
- <id>rpmbuild</id>
- <build>
- <plugins>
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>rpm-maven-plugin</artifactId>
- <executions>
- <execution>
- <id>rpm-osgi-boot</id>
- <phase>package</phase>
- <goals>
- <goal>rpm</goal>
- </goals>
- <configuration>
- <name>argeo-osgi-boot${argeo.rpm.suffix}</name>
- <mappings>
- <mapping>
- <directory>/usr/bin</directory>
- <username>root</username>
- <groupname>root</groupname>
- <filemode>755</filemode>
- <directoryIncluded>false</directoryIncluded>
- <sources>
- <source>
- <location>rpm/usr/bin</location>
- <includes>
- <include>*</include>
- </includes>
- </source>
- </sources>
- </mapping>
- <mapping>
- <directory>/usr/share/osgi/boot</directory>
- <username>root</username>
- <groupname>root</groupname>
- <filemode>644</filemode>
- <directoryIncluded>false</directoryIncluded>
- <dependency>
- <stripVersion>true</stripVersion>
- <includes>
- <include>org.argeo.commons:org.argeo.osgi.boot</include>
- </includes>
- </dependency>
- </mapping>
- <mapping>
- <directory>/usr/share/osgi/boot</directory>
- <username>root</username>
- <groupname>root</groupname>
- <filemode>644</filemode>
- <configuration>noreplace</configuration>
- <directoryIncluded>false</directoryIncluded>
- <sources>
- <source>
- <location>rpm/usr/share/osgi/boot</location>
- <includes>
- <include>*.args</include>
- </includes>
- </source>
- </sources>
- </mapping>
- </mappings>
- <requires>
- <require>argeo-osgi-boot-equinox${argeo.rpm.suffix}</require>
- </requires>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
- </profile>
- <profile>
- <id>rpmbuild-tp</id>
- <build>
- <plugins>
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>rpm-maven-plugin</artifactId>
- <executions>
- <execution>
- <id>rpm-osgi-boot-equinox</id>
- <phase>package</phase>
- <goals>
- <goal>rpm</goal>
- </goals>
- <configuration>
- <name>argeo-osgi-boot-equinox${argeo.rpm.suffix}</name>
- <projversion>${version.argeo-tp}</projversion>
- <release>${argeo.rpm.release.tp}</release>
- <mappings>
- <mapping>
- <directory>/usr/share/osgi/boot</directory>
- <username>root</username>
- <groupname>root</groupname>
- <filemode>644</filemode>
- <directoryIncluded>false</directoryIncluded>
- <dependency>
- <stripVersion>true</stripVersion>
- <includes>
- <include>org.argeo.tp.equinox:org.eclipse.osgi</include>
- </includes>
- </dependency>
- </mapping>
- </mappings>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
- </profile>
- </profiles>
-</project>
+++ /dev/null
-#!/bin/sh
-
-export A2_HOME=$HOME/.a2
-if [ -d "$A2_HOME/share/osgi/boot" ]; then
- PREFIX=$A2_HOME
-else
- PREFIX=/usr
-fi
-
-EQUINOX=$PREFIX/share/osgi/boot/org.eclipse.osgi.jar
-OSGI_BOOT=$PREFIX/share/osgi/boot/org.argeo.osgi.boot.jar
-
-/usr/bin/jshell --class-path "$EQUINOX:$OSGI_BOOT" $*
+++ /dev/null
--jar /usr/share/osgi/boot/org.eclipse.osgi.jar
\ No newline at end of file
<name>Commons Deployable Distributions</name>
<packaging>pom</packaging>
<modules>
- <module>osgi-boot</module>
+ <module>argeo-init</module>
<!-- <module>argeo-cli</module> -->
<module>argeo-node</module>
</modules>
<relativePath>..</relativePath>
</parent>
<artifactId>org.argeo.api</artifactId>
- <name>CMS API</name>
+ <name>CMS APIs</name>
<packaging>jar</packaging>
<dependencies>
</dependencies>
</dependency>
<dependency>
<groupId>org.argeo.commons</groupId>
- <artifactId>org.argeo.enterprise</artifactId>
+ <artifactId>org.argeo.util</artifactId>
<version>2.3-SNAPSHOT</version>
</dependency>
<!-- <dependency> -->
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<classpath>
- <classpathentry kind="src" path="src"/>
- <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
- <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
- <attributes>
- <attribute name="module" value="true"/>
- </attributes>
- </classpathentry>
- <classpathentry kind="output" path="bin"/>
-</classpath>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<projectDescription>
- <name>org.argeo.enterprise</name>
- <comment></comment>
- <projects>
- </projects>
- <buildSpec>
- <buildCommand>
- <name>org.eclipse.jdt.core.javabuilder</name>
- <arguments>
- </arguments>
- </buildCommand>
- <buildCommand>
- <name>org.eclipse.pde.ManifestBuilder</name>
- <arguments>
- </arguments>
- </buildCommand>
- <buildCommand>
- <name>org.eclipse.pde.SchemaBuilder</name>
- <arguments>
- </arguments>
- </buildCommand>
- </buildSpec>
- <natures>
- <nature>org.eclipse.jdt.core.javanature</nature>
- <nature>org.eclipse.pde.PluginNature</nature>
- </natures>
-</projectDescription>
+++ /dev/null
-/MANIFEST.MF
+++ /dev/null
-Bundle-Activator: org.argeo.osgi.internal.EnterpriseActivator
-Bundle-ActivationPolicy: lazy
-
-Import-Package: org.osgi.*;version=0.0.0,\
-!org.apache.commons.logging,\
-*
+++ /dev/null
-source.. = src/
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<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.argeo.commons</groupId>
- <artifactId>argeo-commons</artifactId>
- <version>2.3-SNAPSHOT</version>
- <relativePath>..</relativePath>
- </parent>
- <artifactId>org.argeo.enterprise</artifactId>
- <name>Argeo Enterprise</name>
-</project>
\ No newline at end of file
+++ /dev/null
-package org.argeo.ident;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.net.ConnectException;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.List;
-import java.util.StringTokenizer;
-
-/**
- * A simple ident client, supporting authd OpenSSL encrypted username.
- *
- * @see RFC 1413 https://tools.ietf.org/html/rfc1413
- */
-public class IdentClient {
- public final static int DEFAULT_IDENT_PORT = 113;
- public final static String AUTHD_PASSPHRASE_PATH = "/etc/ident.key";
- final static String NO_USER = "NO-USER";
-
- private final String host;
- private final int port;
-
- private OpenSslDecryptor openSslDecryptor = new OpenSslDecryptor();
- private String identPassphrase = null;
-
- public IdentClient(String host) {
- this(host, readPassphrase(AUTHD_PASSPHRASE_PATH), DEFAULT_IDENT_PORT);
- }
-
- public IdentClient(String host, Path passPhrasePath) {
- this(host, readPassphrase(passPhrasePath), DEFAULT_IDENT_PORT);
- }
-
- public IdentClient(String host, String identPassphrase) {
- this(host, identPassphrase, DEFAULT_IDENT_PORT);
- }
-
- public IdentClient(String host, String identPassphrase, int port) {
- this.host = host;
- this.identPassphrase = identPassphrase;
- this.port = port;
- }
-
- /** @return the username or null if none */
- public String getUsername(int serverPort, int clientPort) {
- String answer;
- try (Socket socket = new Socket(host, port)) {
- String msg = clientPort + "," + serverPort + "\n";
- OutputStream out = socket.getOutputStream();
- out.write(msg.getBytes(StandardCharsets.US_ASCII));
- out.flush();
- BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- answer = reader.readLine();
- } catch (ConnectException e) {
- System.err.println(
- "Ident client is configured but no ident server available on " + host + " (port " + port + ")");
- return null;
- } catch (Exception e) {
- throw new RuntimeException("Cannot read from ident server on " + host + " (port " + port + ")", e);
- }
- StringTokenizer st = new StringTokenizer(answer, " :\n");
- String username = null;
- while (st.hasMoreTokens())
- username = st.nextToken();
-
- if (username.equals(NO_USER))
- return null;
-
- if (identPassphrase != null && username.startsWith("[")) {
- String encrypted = username.substring(1, username.length() - 1);
- username = openSslDecryptor.decryptAuthd(encrypted, identPassphrase).trim();
- }
-// System.out.println(username);
- return username;
- }
-
- public void setOpenSslDecryptor(OpenSslDecryptor openSslDecryptor) {
- this.openSslDecryptor = openSslDecryptor;
- }
-
- public static String readPassphrase(String filePath) {
- return readPassphrase(Paths.get(filePath));
- }
-
- /** @return the first line of the file. */
- public static String readPassphrase(Path path) {
- if (!isPathAvailable(path))
- return null;
- List<String> lines;
- try {
- lines = Files.readAllLines(path);
- } catch (IOException e) {
- throw new IllegalStateException("Cannot read " + path, e);
- }
- if (lines.size() == 0)
- return null;
- String passphrase = lines.get(0);
- return passphrase;
- }
-
- public static boolean isDefaultAuthdPassphraseFileAvailable() {
- return isPathAvailable(Paths.get(AUTHD_PASSPHRASE_PATH));
- }
-
- public static boolean isPathAvailable(Path path) {
- if (!Files.exists(path))
- return false;
- if (!Files.isReadable(path))
- return false;
- return true;
- }
-
- public static void main(String[] args) {
- IdentClient identClient = new IdentClient("127.0.0.1", "changeit");
- String username = identClient.getUsername(7070, 55958);
- System.out.println(username);
- }
-}
+++ /dev/null
-package org.argeo.ident;
-
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
-import java.util.Arrays;
-import java.util.Base64;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.SecretKeySpec;
-
-/**
- * Decrypts OpenSSL encrypted data.
- *
- * From
- * https://stackoverflow.com/questions/11783062/how-to-decrypt-file-in-java-encrypted-with-openssl-command-using-aes
- *
- * See also
- * https://stackoverflow.com/questions/54171959/badpadding-exception-when-trying-to-decrypt-aes-based-encrypted-text/54173509#54173509
- * for new default message digest (not yet in CentOS 7 as of July 2019)
- */
-public class OpenSslDecryptor {
- private static final int INDEX_KEY = 0;
- private static final int INDEX_IV = 1;
- private static final int ITERATIONS = 1;
-
- private static final int SALT_OFFSET = 8;
- private static final int SALT_SIZE = 8;
- private static final int CIPHERTEXT_OFFSET = SALT_OFFSET + SALT_SIZE;
-
- /** In bits. */
- private final int keySize;
-
- private Cipher cipher;
- private MessageDigest messageDigest;
-
- public OpenSslDecryptor() {
- /*
- * Changed to SHA-256 from OpenSSL v1.1.0 (see
- * https://stackoverflow.com/questions/39637388/encryption-decryption-doesnt-
- * work-well-between-two-different-openssl-versions)
- */
- this(128, "MD5");
- }
-
- public OpenSslDecryptor(int keySize, String messageDigest) {
- this.keySize = keySize;
- try {
- this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
- this.messageDigest = MessageDigest.getInstance(messageDigest);
- } catch (GeneralSecurityException e) {
- throw new IllegalArgumentException("Cannot initialise decryptor", e);
- }
- }
-
- public String decryptAuthd(String dataBase64, String passphrase) {
- try {
- byte[] headerSaltAndCipherText = Base64.getDecoder().decode(dataBase64);
-
- boolean withSalt = true;
- byte[] salt = withSalt ? Arrays.copyOfRange(headerSaltAndCipherText, SALT_OFFSET, SALT_OFFSET + SALT_SIZE)
- : null;
- byte[] encrypted = withSalt
- ? Arrays.copyOfRange(headerSaltAndCipherText, CIPHERTEXT_OFFSET, headerSaltAndCipherText.length)
- : headerSaltAndCipherText;
-
- final byte[][] keyAndIV = EVP_BytesToKey(keySize / Byte.SIZE, cipher.getBlockSize(), messageDigest, salt,
- passphrase.getBytes(StandardCharsets.US_ASCII), ITERATIONS);
- SecretKeySpec key = new SecretKeySpec(keyAndIV[INDEX_KEY], "AES");
- IvParameterSpec iv = new IvParameterSpec(keyAndIV[INDEX_IV]);
-
- cipher.init(Cipher.DECRYPT_MODE, key, iv);
- byte[] decrypted = cipher.doFinal(encrypted);
-
- String answer = new String(decrypted, StandardCharsets.US_ASCII);
- return answer;
- } catch (BadPaddingException e) {
- throw new IllegalStateException("Bad password, algorithm, mode or padding;"
- + " no salt, wrong number of iterations or corrupted ciphertext.", e);
- } catch (IllegalBlockSizeException e) {
- throw new IllegalStateException("Bad algorithm, mode or corrupted (resized) ciphertext.", e);
- } catch (GeneralSecurityException e) {
- throw new IllegalStateException(e);
- }
- }
-
- private static byte[][] EVP_BytesToKey(int key_len, int iv_len, MessageDigest md, byte[] salt, byte[] data,
- int count) {
- byte[][] both = new byte[2][];
- byte[] key = new byte[key_len];
- int key_ix = 0;
- byte[] iv = new byte[iv_len];
- int iv_ix = 0;
- both[0] = key;
- both[1] = iv;
- byte[] md_buf = null;
- int nkey = key_len;
- int niv = iv_len;
- int i = 0;
- if (data == null) {
- return both;
- }
- int addmd = 0;
- for (;;) {
- md.reset();
- if (addmd++ > 0) {
- md.update(md_buf);
- }
- md.update(data);
- if (null != salt) {
- md.update(salt, 0, 8);
- }
- md_buf = md.digest();
- for (i = 1; i < count; i++) {
- md.reset();
- md.update(md_buf);
- md_buf = md.digest();
- }
- i = 0;
- if (nkey > 0) {
- for (;;) {
- if (nkey == 0)
- break;
- if (i == md_buf.length)
- break;
- key[key_ix++] = md_buf[i];
- nkey--;
- i++;
- }
- }
- if (niv > 0 && i != md_buf.length) {
- for (;;) {
- if (niv == 0)
- break;
- if (i == md_buf.length)
- break;
- iv[iv_ix++] = md_buf[i];
- niv--;
- i++;
- }
- }
- if (nkey == 0 && niv == 0) {
- break;
- }
- }
- for (i = 0; i < md_buf.length; i++) {
- md_buf[i] = 0;
- }
- return both;
- }
-
- public static void main(String[] args) {
- String dataBase64 = args[0];
- String passphrase = args[1];
- OpenSslDecryptor decryptor = new OpenSslDecryptor();
- System.out.println(decryptor.decryptAuthd(dataBase64, passphrase));
- }
-
-}
\ No newline at end of file
+++ /dev/null
-/** Ident authentication protocol support. */
-package org.argeo.ident;
\ No newline at end of file
+++ /dev/null
-package org.argeo.naming;
-
-import java.util.Dictionary;
-import java.util.Enumeration;
-
-import javax.naming.NamingEnumeration;
-import javax.naming.NamingException;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.naming.directory.BasicAttribute;
-
-public class AttributesDictionary extends Dictionary<String, Object> {
- private final Attributes attributes;
-
- /** The provided attributes is wrapped, not copied. */
- public AttributesDictionary(Attributes attributes) {
- if (attributes == null)
- throw new IllegalArgumentException("Attributes cannot be null");
- this.attributes = attributes;
- }
-
- @Override
- public int size() {
- return attributes.size();
- }
-
- @Override
- public boolean isEmpty() {
- return attributes.size() == 0;
- }
-
- @Override
- public Enumeration<String> keys() {
- NamingEnumeration<String> namingEnumeration = attributes.getIDs();
- return new Enumeration<String>() {
-
- @Override
- public boolean hasMoreElements() {
- return namingEnumeration.hasMoreElements();
- }
-
- @Override
- public String nextElement() {
- return namingEnumeration.nextElement();
- }
-
- };
- }
-
- @Override
- public Enumeration<Object> elements() {
- NamingEnumeration<String> namingEnumeration = attributes.getIDs();
- return new Enumeration<Object>() {
-
- @Override
- public boolean hasMoreElements() {
- return namingEnumeration.hasMoreElements();
- }
-
- @Override
- public Object nextElement() {
- String key = namingEnumeration.nextElement();
- return get(key);
- }
-
- };
- }
-
- @Override
- /** @returns a <code>String</code> or <code>String[]</code> */
- public Object get(Object key) {
- try {
- if (key == null)
- throw new IllegalArgumentException("Key cannot be null");
- Attribute attr = attributes.get(key.toString());
- if (attr == null)
- return null;
- if (attr.size() == 0)
- throw new IllegalStateException("There must be at least one value");
- else if (attr.size() == 1) {
- return attr.get().toString();
- } else {// multiple
- String[] res = new String[attr.size()];
- for (int i = 0; i < attr.size(); i++) {
- Object value = attr.get();
- if (value == null)
- throw new RuntimeException("Values cannot be null");
- res[i] = attr.get(i).toString();
- }
- return res;
- }
- } catch (NamingException e) {
- throw new RuntimeException("Cannot get value for " + key, e);
- }
- }
-
- @Override
- public Object put(String key, Object value) {
- if (key == null)
- throw new IllegalArgumentException("Key cannot be null");
- if (value == null)
- throw new IllegalArgumentException("Value cannot be null");
-
- Object oldValue = get(key);
- Attribute attr = attributes.get(key);
- if (attr == null) {
- attr = new BasicAttribute(key);
- attributes.put(attr);
- }
-
- if (value instanceof String[]) {
- String[] values = (String[]) value;
- // clean additional values
- for (int i = values.length; i < attr.size(); i++)
- attr.remove(i);
- // set values
- for (int i = 0; i < values.length; i++) {
- attr.set(i, values[i]);
- }
- } else {
- if (attr.size() > 1)
- throw new IllegalArgumentException("Attribute " + key + " is multi-valued");
- if (attr.size() == 1) {
- try {
- if (!attr.get(0).equals(value))
- attr.set(0, value.toString());
- } catch (NamingException e) {
- throw new RuntimeException("Cannot check existing value", e);
- }
- } else {
- attr.add(value.toString());
- }
- }
- return oldValue;
- }
-
- @Override
- public Object remove(Object key) {
- if (key == null)
- throw new IllegalArgumentException("Key cannot be null");
- Object oldValue = get(key);
- if (oldValue == null)
- return null;
- return attributes.remove(key.toString());
- }
-
- /**
- * Copy the <b>content</b> of an {@link Attributes} to the provided
- * {@link Dictionary}.
- */
- public static void copy(Attributes attributes, Dictionary<String, Object> dictionary) {
- AttributesDictionary ad = new AttributesDictionary(attributes);
- Enumeration<String> keys = ad.keys();
- while (keys.hasMoreElements()) {
- String key = keys.nextElement();
- dictionary.put(key, ad.get(key));
- }
- }
-
- /**
- * Copy a {@link Dictionary} into an {@link Attributes}.
- */
- public static void copy(Dictionary<String, Object> dictionary, Attributes attributes) {
- AttributesDictionary ad = new AttributesDictionary(attributes);
- Enumeration<String> keys = dictionary.keys();
- while (keys.hasMoreElements()) {
- String key = keys.nextElement();
- ad.put(key, dictionary.get(key));
- }
- }
-}
+++ /dev/null
-package org.argeo.naming;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.StringTokenizer;
-
-import javax.naming.NamingEnumeration;
-import javax.naming.NamingException;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.security.auth.callback.Callback;
-import javax.security.auth.callback.CallbackHandler;
-import javax.security.auth.callback.NameCallback;
-import javax.security.auth.callback.PasswordCallback;
-import javax.security.auth.callback.UnsupportedCallbackException;
-
-import org.argeo.osgi.useradmin.UserDirectoryException;
-
-/** LDAP authPassword field according to RFC 3112 */
-public class AuthPassword implements CallbackHandler {
- private final String authScheme;
- private final String authInfo;
- private final String authValue;
-
- public AuthPassword(String value) {
- StringTokenizer st = new StringTokenizer(value, "$");
- // TODO make it more robust, deal with bad formatting
- this.authScheme = st.nextToken().trim();
- this.authInfo = st.nextToken().trim();
- this.authValue = st.nextToken().trim();
-
- String expectedAuthScheme = getExpectedAuthScheme();
- if (expectedAuthScheme != null && !authScheme.equals(expectedAuthScheme))
- throw new IllegalArgumentException(
- "Auth scheme " + authScheme + " is not compatible with " + expectedAuthScheme);
- }
-
- protected AuthPassword(String authInfo, String authValue) {
- this.authScheme = getExpectedAuthScheme();
- if (authScheme == null)
- throw new IllegalArgumentException("Expected auth scheme cannot be null");
- this.authInfo = authInfo;
- this.authValue = authValue;
- }
-
- protected AuthPassword(AuthPassword authPassword) {
- this.authScheme = authPassword.getAuthScheme();
- this.authInfo = authPassword.getAuthInfo();
- this.authValue = authPassword.getAuthValue();
- }
-
- protected String getExpectedAuthScheme() {
- return null;
- }
-
- protected boolean matchAuthValue(Object object) {
- return authValue.equals(object.toString());
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof AuthPassword))
- return false;
- AuthPassword authPassword = (AuthPassword) obj;
- return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo)
- && authValue.equals(authValue);
- }
-
- public boolean keyEquals(AuthPassword authPassword) {
- return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo);
- }
-
- @Override
- public int hashCode() {
- return authValue.hashCode();
- }
-
- @Override
- public String toString() {
- return toAuthPassword();
- }
-
- public final String toAuthPassword() {
- return getAuthScheme() + '$' + authInfo + '$' + authValue;
- }
-
- public String getAuthScheme() {
- return authScheme;
- }
-
- public String getAuthInfo() {
- return authInfo;
- }
-
- public String getAuthValue() {
- return authValue;
- }
-
- public static AuthPassword matchAuthValue(Attributes attributes, char[] value) {
- try {
- Attribute authPassword = attributes.get(LdapAttrs.authPassword.name());
- if (authPassword != null) {
- NamingEnumeration<?> values = authPassword.getAll();
- while (values.hasMore()) {
- Object val = values.next();
- AuthPassword token = new AuthPassword(val.toString());
- String auth;
- if (Arrays.binarySearch(value, '$') >= 0) {
- auth = token.authInfo + '$' + token.authValue;
- } else {
- auth = token.authValue;
- }
- if (Arrays.equals(auth.toCharArray(), value))
- return token;
- // if (token.matchAuthValue(value))
- // return token;
- }
- }
- return null;
- } catch (NamingException e) {
- throw new UserDirectoryException("Cannot check attribute", e);
- }
- }
-
- public static boolean remove(Attributes attributes, AuthPassword value) {
- Attribute authPassword = attributes.get(LdapAttrs.authPassword.name());
- return authPassword.remove(value.toAuthPassword());
- }
-
- @Override
- public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
- for (Callback callback : callbacks) {
- if (callback instanceof NameCallback)
- ((NameCallback) callback).setName(toAuthPassword());
- else if (callback instanceof PasswordCallback)
- ((PasswordCallback) callback).setPassword(getAuthValue().toCharArray());
- }
- }
-
-}
+++ /dev/null
-package org.argeo.naming;
-
-import java.util.EnumSet;
-import java.util.Set;
-import java.util.TreeSet;
-
-import javax.naming.InvalidNameException;
-import javax.naming.ldap.LdapName;
-
-/**
- * An object that can be identified with an X.500 distinguished name.
- *
- * @see https://tools.ietf.org/html/rfc1779
- */
-public interface Distinguished {
- /** The related distinguished name. */
- String dn();
-
- /** The related distinguished name as an {@link LdapName}. */
- default LdapName distinguishedName() {
- try {
- return new LdapName(dn());
- } catch (InvalidNameException e) {
- throw new IllegalArgumentException("Distinguished name " + dn() + " is not properly formatted.", e);
- }
- }
-
- /** List all DNs of an enumeration as strings. */
- static Set<String> enumToDns(EnumSet<? extends Distinguished> enumSet) {
- Set<String> res = new TreeSet<>();
- for (Enum<? extends Distinguished> enm : enumSet) {
- res.add(((Distinguished) enm).dn());
- }
- return res;
- }
-}
+++ /dev/null
-package org.argeo.naming;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Collections;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-import java.util.SortedSet;
-import java.util.TreeMap;
-import java.util.TreeSet;
-
-import javax.naming.Binding;
-import javax.naming.NamingEnumeration;
-import javax.naming.NamingException;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.naming.directory.DirContext;
-import javax.naming.directory.InitialDirContext;
-
-public class DnsBrowser implements Closeable {
- private final DirContext initialCtx;
-
- public DnsBrowser() throws NamingException {
- this(null);
- }
-
- public DnsBrowser(String dnsServerUrls) throws NamingException {
- Hashtable<String, Object> env = new Hashtable<>();
- env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
- if (dnsServerUrls != null)
- env.put("java.naming.provider.url", dnsServerUrls);
- initialCtx = new InitialDirContext(env);
- }
-
- public Map<String, List<String>> getAllRecords(String name) throws NamingException {
- Map<String, List<String>> res = new TreeMap<>();
- Attributes attrs = initialCtx.getAttributes(name);
- NamingEnumeration<String> ids = attrs.getIDs();
- while (ids.hasMore()) {
- String recordType = ids.next();
- List<String> lst = new ArrayList<String>();
- res.put(recordType, lst);
- Attribute attr = attrs.get(recordType);
- addValues(attr, lst);
- }
- return Collections.unmodifiableMap(res);
- }
-
- /**
- * Return a single record (typically A, AAAA, etc. or null if not available.
- * Will fail if multiple records.
- */
- public String getRecord(String name, String recordType) throws NamingException {
- Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType });
- if (attrs.size() == 0)
- return null;
- Attribute attr = attrs.get(recordType);
- if (attr.size() > 1)
- throw new IllegalArgumentException("Multiple record type " + recordType);
- assert attr.size() != 0;
- Object value = attr.get();
- assert value != null;
- return value.toString();
- }
-
- /**
- * Return records of a given type.
- */
- public List<String> getRecords(String name, String recordType) throws NamingException {
- List<String> res = new ArrayList<String>();
- Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType });
- Attribute attr = attrs.get(recordType);
- addValues(attr, res);
- return res;
- }
-
- /** Ordered, with preferred first. */
- public List<String> getSrvRecordsAsHosts(String name, boolean withPort) throws NamingException {
- List<String> raw = getRecords(name, "SRV");
- if (raw.size() == 0)
- return null;
- SortedSet<SrvRecord> res = new TreeSet<>();
- for (int i = 0; i < raw.size(); i++) {
- String record = raw.get(i);
- String[] arr = record.split(" ");
- Integer priority = Integer.parseInt(arr[0]);
- Integer weight = Integer.parseInt(arr[1]);
- Integer port = Integer.parseInt(arr[2]);
- String hostname = arr[3];
- SrvRecord order = new SrvRecord(priority, weight, port, hostname);
- res.add(order);
- }
- List<String> lst = new ArrayList<>();
- for (SrvRecord order : res) {
- lst.add(order.toHost(withPort));
- }
- return Collections.unmodifiableList(lst);
- }
-
- private void addValues(Attribute attr, List<String> lst) throws NamingException {
- NamingEnumeration<?> values = attr.getAll();
- while (values.hasMore()) {
- Object value = values.next();
- if (value != null) {
- if (value instanceof byte[]) {
- String str = Base64.getEncoder().encodeToString((byte[]) value);
- lst.add(str);
- } else
- lst.add(value.toString());
- }
- }
-
- }
-
- public List<String> listEntries(String name) throws NamingException {
- List<String> res = new ArrayList<String>();
- NamingEnumeration<Binding> ne = initialCtx.listBindings(name);
- while (ne.hasMore()) {
- Binding b = ne.next();
- res.add(b.getName());
- }
- return Collections.unmodifiableList(res);
- }
-
- @Override
- public void close() throws IOException {
- destroy();
- }
-
- public void destroy() {
- try {
- initialCtx.close();
- } catch (NamingException e) {
- // silent
- }
- }
-
- public static void main(String[] args) {
- if (args.length == 0) {
- printUsage(System.err);
- System.exit(1);
- }
- try (DnsBrowser dnsBrowser = new DnsBrowser()) {
- String hostname = args[0];
- String recordType = args.length > 1 ? args[1] : "A";
- if (recordType.equals("*")) {
- Map<String, List<String>> records = dnsBrowser.getAllRecords(hostname);
- for (String type : records.keySet()) {
- for (String record : records.get(type)) {
- String typeLabel;
- if ("44".equals(type))
- typeLabel = "SSHFP";
- else if ("46".equals(type))
- typeLabel = "RRSIG";
- else if ("48".equals(type))
- typeLabel = "DNSKEY";
- else
- typeLabel = type;
- System.out.println(typeLabel + "\t" + record);
- }
- }
- } else {
- System.out.println(dnsBrowser.getRecord(hostname, recordType));
- }
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-
- public static void printUsage(PrintStream out) {
- out.println("java org.argeo.naming.DnsBrowser <hostname> [<record type> | *]");
- }
-
-}
\ No newline at end of file
+++ /dev/null
-uid,,,0.9.2342.19200300.100.1.1,,RFC 4519
-mail,,,0.9.2342.19200300.100.1.3,,RFC 4524
-info,,,0.9.2342.19200300.100.1.4,,RFC 4524
-drink,,,0.9.2342.19200300.100.1.5,,RFC 4524
-roomNumber,,,0.9.2342.19200300.100.1.6,,RFC 4524
-photo,,,0.9.2342.19200300.100.1.7,,RFC 2798
-userClass,,,0.9.2342.19200300.100.1.8,,RFC 4524
-host,,,0.9.2342.19200300.100.1.9,,RFC 4524
-manager,,,0.9.2342.19200300.100.1.10,,RFC 4524
-documentIdentifier,,,0.9.2342.19200300.100.1.11,,RFC 4524
-documentTitle,,,0.9.2342.19200300.100.1.12,,RFC 4524
-documentVersion,,,0.9.2342.19200300.100.1.13,,RFC 4524
-documentAuthor,,,0.9.2342.19200300.100.1.14,,RFC 4524
-documentLocation,,,0.9.2342.19200300.100.1.15,,RFC 4524
-homePhone,,,0.9.2342.19200300.100.1.20,,RFC 4524
-secretary,,,0.9.2342.19200300.100.1.21,,RFC 4524
-dc,,,0.9.2342.19200300.100.1.25,,RFC 4519
-associatedDomain,,,0.9.2342.19200300.100.1.37,,RFC 4524
-associatedName,,,0.9.2342.19200300.100.1.38,,RFC 4524
-homePostalAddress,,,0.9.2342.19200300.100.1.39,,RFC 4524
-personalTitle,,,0.9.2342.19200300.100.1.40,,RFC 4524
-mobile,,,0.9.2342.19200300.100.1.41,,RFC 4524
-pager,,,0.9.2342.19200300.100.1.42,,RFC 4524
-co,,,0.9.2342.19200300.100.1.43,,RFC 4524
-uniqueIdentifier,,,0.9.2342.19200300.100.1.44,,RFC 4524
-organizationalStatus,,,0.9.2342.19200300.100.1.45,,RFC 4524
-buildingName,,,0.9.2342.19200300.100.1.48,,RFC 4524
-audio,,,0.9.2342.19200300.100.1.55,,RFC 2798
-documentPublisher,,,0.9.2342.19200300.100.1.56,,RFC 4524
-jpegPhoto,,,0.9.2342.19200300.100.1.60,,RFC 2798
-vendorName,,,1.3.6.1.1.4,,RFC 3045
-vendorVersion,,,1.3.6.1.1.5,,RFC 3045
-entryUUID,,,1.3.6.1.1.16.4,,RFC 4530
-entryDN,,,1.3.6.1.1.20,,RFC 5020
-labeledURI,,,1.3.6.1.4.1.250.1.57,,RFC 2798
-numSubordinates,,,1.3.6.1.4.1.453.16.2.103,,draft-ietf-boreham-numsubordinates
-namingContexts,,,1.3.6.1.4.1.1466.101.120.5,,RFC 4512
-altServer,,,1.3.6.1.4.1.1466.101.120.6,,RFC 4512
-supportedExtension,,,1.3.6.1.4.1.1466.101.120.7,,RFC 4512
-supportedControl,,,1.3.6.1.4.1.1466.101.120.13,,RFC 4512
-supportedSASLMechanisms,,,1.3.6.1.4.1.1466.101.120.14,,RFC 4512
-supportedLDAPVersion,,,1.3.6.1.4.1.1466.101.120.15,,RFC 4512
-ldapSyntaxes,,,1.3.6.1.4.1.1466.101.120.16,,RFC 4512
-supportedAuthPasswordSchemes,,,1.3.6.1.4.1.4203.1.3.3,,RFC 3112
-authPassword,,,1.3.6.1.4.1.4203.1.3.4,,RFC 3112
-supportedFeatures,,,1.3.6.1.4.1.4203.1.3.5,,RFC 4512
-inheritable,,,1.3.6.1.4.1.7628.5.4.1,,draft-ietf-ldup-subentry
-blockInheritance,,,1.3.6.1.4.1.7628.5.4.2,,draft-ietf-ldup-subentry
-objectClass,,,2.5.4.0,,RFC 4512
-aliasedObjectName,,,2.5.4.1,,RFC 4512
-cn,,,2.5.4.3,,RFC 4519
-sn,,,2.5.4.4,,RFC 4519
-serialNumber,,,2.5.4.5,,RFC 4519
-c,,,2.5.4.6,,RFC 4519
-l,,,2.5.4.7,,RFC 4519
-st,,,2.5.4.8,,RFC 4519
-street,,,2.5.4.9,,RFC 4519
-o,,,2.5.4.10,,RFC 4519
-ou,,,2.5.4.11,,RFC 4519
-title,,,2.5.4.12,,RFC 4519
-description,,,2.5.4.13,,RFC 4519
-searchGuide,,,2.5.4.14,,RFC 4519
-businessCategory,,,2.5.4.15,,RFC 4519
-postalAddress,,,2.5.4.16,,RFC 4519
-postalCode,,,2.5.4.17,,RFC 4519
-postOfficeBox,,,2.5.4.18,,RFC 4519
-physicalDeliveryOfficeName,,,2.5.4.19,,RFC 4519
-telephoneNumber,,,2.5.4.20,,RFC 4519
-telexNumber,,,2.5.4.21,,RFC 4519
-teletexTerminalIdentifier,,,2.5.4.22,,RFC 4519
-facsimileTelephoneNumber,,,2.5.4.23,,RFC 4519
-x121Address,,,2.5.4.24,,RFC 4519
-internationalISDNNumber,,,2.5.4.25,,RFC 4519
-registeredAddress,,,2.5.4.26,,RFC 4519
-destinationIndicator,,,2.5.4.27,,RFC 4519
-preferredDeliveryMethod,,,2.5.4.28,,RFC 4519
-member,,,2.5.4.31,,RFC 4519
-owner,,,2.5.4.32,,RFC 4519
-roleOccupant,,,2.5.4.33,,RFC 4519
-seeAlso,,,2.5.4.34,,RFC 4519
-userPassword,,,2.5.4.35,,RFC 4519
-userCertificate,,,2.5.4.36,,RFC 4523
-cACertificate,,,2.5.4.37,,RFC 4523
-authorityRevocationList,,,2.5.4.38,,RFC 4523
-certificateRevocationList,,,2.5.4.39,,RFC 4523
-crossCertificatePair,,,2.5.4.40,,RFC 4523
-name,,,2.5.4.41,,RFC 4519
-givenName,,,2.5.4.42,,RFC 4519
-initials,,,2.5.4.43,,RFC 4519
-generationQualifier,,,2.5.4.44,,RFC 4519
-x500UniqueIdentifier,,,2.5.4.45,,RFC 4519
-dnQualifier,,,2.5.4.46,,RFC 4519
-enhancedSearchGuide,,,2.5.4.47,,RFC 4519
-distinguishedName,,,2.5.4.49,,RFC 4519
-uniqueMember,,,2.5.4.50,,RFC 4519
-houseIdentifier,,,2.5.4.51,,RFC 4519
-supportedAlgorithms,,,2.5.4.52,,RFC 4523
-deltaRevocationList,,,2.5.4.53,,RFC 4523
-createTimestamp,,,2.5.18.1,,RFC 4512
-modifyTimestamp,,,2.5.18.2,,RFC 4512
-creatorsName,,,2.5.18.3,,RFC 4512
-modifiersName,,,2.5.18.4,,RFC 4512
-subschemaSubentry,,,2.5.18.10,,RFC 4512
-dITStructureRules,,,2.5.21.1,,RFC 4512
-dITContentRules,,,2.5.21.2,,RFC 4512
-matchingRules,,,2.5.21.4,,RFC 4512
-attributeTypes,,,2.5.21.5,,RFC 4512
-objectClasses,,,2.5.21.6,,RFC 4512
-nameForms,,,2.5.21.7,,RFC 4512
-matchingRuleUse,,,2.5.21.8,,RFC 4512
-structuralObjectClass,,,2.5.21.9,,RFC 4512
-governingStructureRule,,,2.5.21.10,,RFC 4512
-carLicense,,,2.16.840.1.113730.3.1.1,,RFC 2798
-departmentNumber,,,2.16.840.1.113730.3.1.2,,RFC 2798
-employeeNumber,,,2.16.840.1.113730.3.1.3,,RFC 2798
-employeeType,,,2.16.840.1.113730.3.1.4,,RFC 2798
-changeNumber,,,2.16.840.1.113730.3.1.5,,draft-good-ldap-changelog
-targetDN,,,2.16.840.1.113730.3.1.6,,draft-good-ldap-changelog
-changeType,,,2.16.840.1.113730.3.1.7,,draft-good-ldap-changelog
-changes,,,2.16.840.1.113730.3.1.8,,draft-good-ldap-changelog
-newRDN,,,2.16.840.1.113730.3.1.9,,draft-good-ldap-changelog
-deleteOldRDN,,,2.16.840.1.113730.3.1.10,,draft-good-ldap-changelog
-newSuperior,,,2.16.840.1.113730.3.1.11,,draft-good-ldap-changelog
-ref,,,2.16.840.1.113730.3.1.34,,RFC 3296
-changelog,,,2.16.840.1.113730.3.1.35,,draft-good-ldap-changelog
-preferredLanguage,,,2.16.840.1.113730.3.1.39,,RFC 2798
-userSMIMECertificate,,,2.16.840.1.113730.3.1.40,,RFC 2798
-userPKCS12,,,2.16.840.1.113730.3.1.216,,RFC 2798
-displayName,,,2.16.840.1.113730.3.1.241,,RFC 2798
+++ /dev/null
-package org.argeo.naming;
-
-/**
- * Standard LDAP attributes as per:<br>
- * - <a href= "https://www.ldap.com/ldap-oid-reference">Standard LDAP</a><br>
- * - <a href=
- * "https://github.com/krb5/krb5/blob/master/src/plugins/kdb/ldap/libkdb_ldap/kerberos.schema">Kerberos
- * LDAP (partial)</a>
- */
-public enum LdapAttrs implements SpecifiedName {
- /** */
- uid("0.9.2342.19200300.100.1.1", "RFC 4519"),
- /** */
- mail("0.9.2342.19200300.100.1.3", "RFC 4524"),
- /** */
- info("0.9.2342.19200300.100.1.4", "RFC 4524"),
- /** */
- drink("0.9.2342.19200300.100.1.5", "RFC 4524"),
- /** */
- roomNumber("0.9.2342.19200300.100.1.6", "RFC 4524"),
- /** */
- photo("0.9.2342.19200300.100.1.7", "RFC 2798"),
- /** */
- userClass("0.9.2342.19200300.100.1.8", "RFC 4524"),
- /** */
- host("0.9.2342.19200300.100.1.9", "RFC 4524"),
- /** */
- manager("0.9.2342.19200300.100.1.10", "RFC 4524"),
- /** */
- documentIdentifier("0.9.2342.19200300.100.1.11", "RFC 4524"),
- /** */
- documentTitle("0.9.2342.19200300.100.1.12", "RFC 4524"),
- /** */
- documentVersion("0.9.2342.19200300.100.1.13", "RFC 4524"),
- /** */
- documentAuthor("0.9.2342.19200300.100.1.14", "RFC 4524"),
- /** */
- documentLocation("0.9.2342.19200300.100.1.15", "RFC 4524"),
- /** */
- homePhone("0.9.2342.19200300.100.1.20", "RFC 4524"),
- /** */
- secretary("0.9.2342.19200300.100.1.21", "RFC 4524"),
- /** */
- dc("0.9.2342.19200300.100.1.25", "RFC 4519"),
- /** */
- associatedDomain("0.9.2342.19200300.100.1.37", "RFC 4524"),
- /** */
- associatedName("0.9.2342.19200300.100.1.38", "RFC 4524"),
- /** */
- homePostalAddress("0.9.2342.19200300.100.1.39", "RFC 4524"),
- /** */
- personalTitle("0.9.2342.19200300.100.1.40", "RFC 4524"),
- /** */
- mobile("0.9.2342.19200300.100.1.41", "RFC 4524"),
- /** */
- pager("0.9.2342.19200300.100.1.42", "RFC 4524"),
- /** */
- co("0.9.2342.19200300.100.1.43", "RFC 4524"),
- /** */
- uniqueIdentifier("0.9.2342.19200300.100.1.44", "RFC 4524"),
- /** */
- organizationalStatus("0.9.2342.19200300.100.1.45", "RFC 4524"),
- /** */
- buildingName("0.9.2342.19200300.100.1.48", "RFC 4524"),
- /** */
- audio("0.9.2342.19200300.100.1.55", "RFC 2798"),
- /** */
- documentPublisher("0.9.2342.19200300.100.1.56", "RFC 4524"),
- /** */
- jpegPhoto("0.9.2342.19200300.100.1.60", "RFC 2798"),
- /** */
- vendorName("1.3.6.1.1.4", "RFC 3045"),
- /** */
- vendorVersion("1.3.6.1.1.5", "RFC 3045"),
- /** */
- entryUUID("1.3.6.1.1.16.4", "RFC 4530"),
- /** */
- entryDN("1.3.6.1.1.20", "RFC 5020"),
- /** */
- labeledURI("1.3.6.1.4.1.250.1.57", "RFC 2798"),
- /** */
- numSubordinates("1.3.6.1.4.1.453.16.2.103", "draft-ietf-boreham-numsubordinates"),
- /** */
- namingContexts("1.3.6.1.4.1.1466.101.120.5", "RFC 4512"),
- /** */
- altServer("1.3.6.1.4.1.1466.101.120.6", "RFC 4512"),
- /** */
- supportedExtension("1.3.6.1.4.1.1466.101.120.7", "RFC 4512"),
- /** */
- supportedControl("1.3.6.1.4.1.1466.101.120.13", "RFC 4512"),
- /** */
- supportedSASLMechanisms("1.3.6.1.4.1.1466.101.120.14", "RFC 4512"),
- /** */
- supportedLDAPVersion("1.3.6.1.4.1.1466.101.120.15", "RFC 4512"),
- /** */
- ldapSyntaxes("1.3.6.1.4.1.1466.101.120.16", "RFC 4512"),
- /** */
- supportedAuthPasswordSchemes("1.3.6.1.4.1.4203.1.3.3", "RFC 3112"),
- /** */
- authPassword("1.3.6.1.4.1.4203.1.3.4", "RFC 3112"),
- /** */
- supportedFeatures("1.3.6.1.4.1.4203.1.3.5", "RFC 4512"),
- /** */
- inheritable("1.3.6.1.4.1.7628.5.4.1", "draft-ietf-ldup-subentry"),
- /** */
- blockInheritance("1.3.6.1.4.1.7628.5.4.2", "draft-ietf-ldup-subentry"),
- /** */
- objectClass("2.5.4.0", "RFC 4512"),
- /** */
- aliasedObjectName("2.5.4.1", "RFC 4512"),
- /** */
- cn("2.5.4.3", "RFC 4519"),
- /** */
- sn("2.5.4.4", "RFC 4519"),
- /** */
- serialNumber("2.5.4.5", "RFC 4519"),
- /** */
- c("2.5.4.6", "RFC 4519"),
- /** */
- l("2.5.4.7", "RFC 4519"),
- /** */
- st("2.5.4.8", "RFC 4519"),
- /** */
- street("2.5.4.9", "RFC 4519"),
- /** */
- o("2.5.4.10", "RFC 4519"),
- /** */
- ou("2.5.4.11", "RFC 4519"),
- /** */
- title("2.5.4.12", "RFC 4519"),
- /** */
- description("2.5.4.13", "RFC 4519"),
- /** */
- searchGuide("2.5.4.14", "RFC 4519"),
- /** */
- businessCategory("2.5.4.15", "RFC 4519"),
- /** */
- postalAddress("2.5.4.16", "RFC 4519"),
- /** */
- postalCode("2.5.4.17", "RFC 4519"),
- /** */
- postOfficeBox("2.5.4.18", "RFC 4519"),
- /** */
- physicalDeliveryOfficeName("2.5.4.19", "RFC 4519"),
- /** */
- telephoneNumber("2.5.4.20", "RFC 4519"),
- /** */
- telexNumber("2.5.4.21", "RFC 4519"),
- /** */
- teletexTerminalIdentifier("2.5.4.22", "RFC 4519"),
- /** */
- facsimileTelephoneNumber("2.5.4.23", "RFC 4519"),
- /** */
- x121Address("2.5.4.24", "RFC 4519"),
- /** */
- internationalISDNNumber("2.5.4.25", "RFC 4519"),
- /** */
- registeredAddress("2.5.4.26", "RFC 4519"),
- /** */
- destinationIndicator("2.5.4.27", "RFC 4519"),
- /** */
- preferredDeliveryMethod("2.5.4.28", "RFC 4519"),
- /** */
- member("2.5.4.31", "RFC 4519"),
- /** */
- owner("2.5.4.32", "RFC 4519"),
- /** */
- roleOccupant("2.5.4.33", "RFC 4519"),
- /** */
- seeAlso("2.5.4.34", "RFC 4519"),
- /** */
- userPassword("2.5.4.35", "RFC 4519"),
- /** */
- userCertificate("2.5.4.36", "RFC 4523"),
- /** */
- cACertificate("2.5.4.37", "RFC 4523"),
- /** */
- authorityRevocationList("2.5.4.38", "RFC 4523"),
- /** */
- certificateRevocationList("2.5.4.39", "RFC 4523"),
- /** */
- crossCertificatePair("2.5.4.40", "RFC 4523"),
- /** */
- name("2.5.4.41", "RFC 4519"),
- /** */
- givenName("2.5.4.42", "RFC 4519"),
- /** */
- initials("2.5.4.43", "RFC 4519"),
- /** */
- generationQualifier("2.5.4.44", "RFC 4519"),
- /** */
- x500UniqueIdentifier("2.5.4.45", "RFC 4519"),
- /** */
- dnQualifier("2.5.4.46", "RFC 4519"),
- /** */
- enhancedSearchGuide("2.5.4.47", "RFC 4519"),
- /** */
- distinguishedName("2.5.4.49", "RFC 4519"),
- /** */
- uniqueMember("2.5.4.50", "RFC 4519"),
- /** */
- houseIdentifier("2.5.4.51", "RFC 4519"),
- /** */
- supportedAlgorithms("2.5.4.52", "RFC 4523"),
- /** */
- deltaRevocationList("2.5.4.53", "RFC 4523"),
- /** */
- createTimestamp("2.5.18.1", "RFC 4512"),
- /** */
- modifyTimestamp("2.5.18.2", "RFC 4512"),
- /** */
- creatorsName("2.5.18.3", "RFC 4512"),
- /** */
- modifiersName("2.5.18.4", "RFC 4512"),
- /** */
- subschemaSubentry("2.5.18.10", "RFC 4512"),
- /** */
- dITStructureRules("2.5.21.1", "RFC 4512"),
- /** */
- dITContentRules("2.5.21.2", "RFC 4512"),
- /** */
- matchingRules("2.5.21.4", "RFC 4512"),
- /** */
- attributeTypes("2.5.21.5", "RFC 4512"),
- /** */
- objectClasses("2.5.21.6", "RFC 4512"),
- /** */
- nameForms("2.5.21.7", "RFC 4512"),
- /** */
- matchingRuleUse("2.5.21.8", "RFC 4512"),
- /** */
- structuralObjectClass("2.5.21.9", "RFC 4512"),
- /** */
- governingStructureRule("2.5.21.10", "RFC 4512"),
- /** */
- carLicense("2.16.840.1.113730.3.1.1", "RFC 2798"),
- /** */
- departmentNumber("2.16.840.1.113730.3.1.2", "RFC 2798"),
- /** */
- employeeNumber("2.16.840.1.113730.3.1.3", "RFC 2798"),
- /** */
- employeeType("2.16.840.1.113730.3.1.4", "RFC 2798"),
- /** */
- changeNumber("2.16.840.1.113730.3.1.5", "draft-good-ldap-changelog"),
- /** */
- targetDN("2.16.840.1.113730.3.1.6", "draft-good-ldap-changelog"),
- /** */
- changeType("2.16.840.1.113730.3.1.7", "draft-good-ldap-changelog"),
- /** */
- changes("2.16.840.1.113730.3.1.8", "draft-good-ldap-changelog"),
- /** */
- newRDN("2.16.840.1.113730.3.1.9", "draft-good-ldap-changelog"),
- /** */
- deleteOldRDN("2.16.840.1.113730.3.1.10", "draft-good-ldap-changelog"),
- /** */
- newSuperior("2.16.840.1.113730.3.1.11", "draft-good-ldap-changelog"),
- /** */
- ref("2.16.840.1.113730.3.1.34", "RFC 3296"),
- /** */
- changelog("2.16.840.1.113730.3.1.35", "draft-good-ldap-changelog"),
- /** */
- preferredLanguage("2.16.840.1.113730.3.1.39", "RFC 2798"),
- /** */
- userSMIMECertificate("2.16.840.1.113730.3.1.40", "RFC 2798"),
- /** */
- userPKCS12("2.16.840.1.113730.3.1.216", "RFC 2798"),
- /** */
- displayName("2.16.840.1.113730.3.1.241", "RFC 2798"),
-
- // Sun memberOf
- memberOf("1.2.840.113556.1.2.102", "389 DS memberOf"),
-
- // KERBEROS (partial)
- krbPrincipalName("2.16.840.1.113719.1.301.6.8.1", "Novell Kerberos Schema Definitions"),
-
- // RFC 2985 and RFC 3039 (partial)
- dateOfBirth("1.3.6.1.5.5.7.9.1", "RFC 2985"),
- /** */
- placeOfBirth("1.3.6.1.5.5.7.9.2", "RFC 2985"),
- /** */
- gender("1.3.6.1.5.5.7.9.3", "RFC 2985"),
- /** */
- countryOfCitizenship("1.3.6.1.5.5.7.9.4", "RFC 2985"),
- /** */
- countryOfResidence("1.3.6.1.5.5.7.9.5", "RFC 2985"),
- //
- ;
-
- public final static String DN = "dn";
-
-// private final static String LDAP_ = "ldap:";
-
- private final String oid, spec;
-
- LdapAttrs(String oid, String spec) {
- this.oid = oid;
- this.spec = spec;
- }
-
- @Override
- public String getID() {
- return oid;
- }
-
- @Override
- public String getSpec() {
- return spec;
- }
-
- public String getPrefix() {
- return prefix();
- }
-
- public static String prefix() {
- return "ldap";
- }
-
- public String property() {
- return qualified();
- }
-
- public String qualified() {
- String prefix = getPrefix();
- return prefix != null ? prefix + ":" + name() : name();
- }
-
- public String getNamespace() {
- return namespace();
- }
-
- public static String namespace() {
- return "http://www.argeo.org/ns/ldap";
- }
-
- @Override
- public final String toString() {
- // must return the name
- return name();
- }
-
-}
+++ /dev/null
-account,,,0.9.2342.19200300.100.4.5,,RFC 4524
-document,,,0.9.2342.19200300.100.4.6,,RFC 4524
-room,,,0.9.2342.19200300.100.4.7,,RFC 4524
-documentSeries,,,0.9.2342.19200300.100.4.9,,RFC 4524
-domain,,,0.9.2342.19200300.100.4.13,,RFC 4524
-rFC822localPart,,,0.9.2342.19200300.100.4.14,,RFC 4524
-domainRelatedObject,,,0.9.2342.19200300.100.4.17,,RFC 4524
-friendlyCountry,,,0.9.2342.19200300.100.4.18,,RFC 4524
-simpleSecurityObject,,,0.9.2342.19200300.100.4.19,,RFC 4524
-uidObject,,,1.3.6.1.1.3.1,,RFC 4519
-extensibleObject,,,1.3.6.1.4.1.1466.101.120.111,,RFC 4512
-dcObject,,,1.3.6.1.4.1.1466.344,,RFC 4519
-authPasswordObject,,,1.3.6.1.4.1.4203.1.4.7,,RFC 3112
-namedObject,,,1.3.6.1.4.1.5322.13.1.1,,draft-howard-namedobject
-inheritableLDAPSubEntry,,,1.3.6.1.4.1.7628.5.6.1.1,,draft-ietf-ldup-subentry
-top,,,2.5.6.0,,RFC 4512
-alias,,,2.5.6.1,,RFC 4512
-country,,,2.5.6.2,,RFC 4519
-locality,,,2.5.6.3,,RFC 4519
-organization,,,2.5.6.4,,RFC 4519
-organizationalUnit,,,2.5.6.5,,RFC 4519
-person,,,2.5.6.6,,RFC 4519
-organizationalPerson,,,2.5.6.7,,RFC 4519
-organizationalRole,,,2.5.6.8,,RFC 4519
-groupOfNames,,,2.5.6.9,,RFC 4519
-residentialPerson,,,2.5.6.10,,RFC 4519
-applicationProcess,,,2.5.6.11,,RFC 4519
-device,,,2.5.6.14,,RFC 4519
-strongAuthenticationUser,,,2.5.6.15,,RFC 4523
-certificationAuthority,,,2.5.6.16,,RFC 4523
-certificationAuthority-V2,,,2.5.6.16.2,,RFC 4523
-groupOfUniqueNames,,,2.5.6.17,,RFC 4519
-userSecurityInformation,,,2.5.6.18,,RFC 4523
-cRLDistributionPoint,,,2.5.6.19,,RFC 4523
-pkiUser,,,2.5.6.21,,RFC 4523
-pkiCA,,,2.5.6.22,,RFC 4523
-deltaCRL,,,2.5.6.23,,RFC 4523
-subschema,,,2.5.20.1,,RFC 4512
-ldapSubEntry,,,2.16.840.1.113719.2.142.6.1.1,,draft-ietf-ldup-subentry
-changeLogEntry,,,2.16.840.1.113730.3.2.1,,draft-good-ldap-changelog
-inetOrgPerson,,,2.16.840.1.113730.3.2.2,,RFC 2798
-referral,,,2.16.840.1.113730.3.2.6,,RFC 3296
+++ /dev/null
-package org.argeo.naming;
-
-/**
- * Standard LDAP object classes as per
- * <a href="https://www.ldap.com/ldap-oid-reference">https://www.ldap.com/ldap-
- * oid-reference</a>
- */
-public enum LdapObjs implements SpecifiedName {
- account("0.9.2342.19200300.100.4.5", "RFC 4524"),
- /** */
- document("0.9.2342.19200300.100.4.6", "RFC 4524"),
- /** */
- room("0.9.2342.19200300.100.4.7", "RFC 4524"),
- /** */
- documentSeries("0.9.2342.19200300.100.4.9", "RFC 4524"),
- /** */
- domain("0.9.2342.19200300.100.4.13", "RFC 4524"),
- /** */
- rFC822localPart("0.9.2342.19200300.100.4.14", "RFC 4524"),
- /** */
- domainRelatedObject("0.9.2342.19200300.100.4.17", "RFC 4524"),
- /** */
- friendlyCountry("0.9.2342.19200300.100.4.18", "RFC 4524"),
- /** */
- simpleSecurityObject("0.9.2342.19200300.100.4.19", "RFC 4524"),
- /** */
- uidObject("1.3.6.1.1.3.1", "RFC 4519"),
- /** */
- extensibleObject("1.3.6.1.4.1.1466.101.120.111", "RFC 4512"),
- /** */
- dcObject("1.3.6.1.4.1.1466.344", "RFC 4519"),
- /** */
- authPasswordObject("1.3.6.1.4.1.4203.1.4.7", "RFC 3112"),
- /** */
- namedObject("1.3.6.1.4.1.5322.13.1.1", "draft-howard-namedobject"),
- /** */
- inheritableLDAPSubEntry("1.3.6.1.4.1.7628.5.6.1.1", "draft-ietf-ldup-subentry"),
- /** */
- top("2.5.6.0", "RFC 4512"),
- /** */
- alias("2.5.6.1", "RFC 4512"),
- /** */
- country("2.5.6.2", "RFC 4519"),
- /** */
- locality("2.5.6.3", "RFC 4519"),
- /** */
- organization("2.5.6.4", "RFC 4519"),
- /** */
- organizationalUnit("2.5.6.5", "RFC 4519"),
- /** */
- person("2.5.6.6", "RFC 4519"),
- /** */
- organizationalPerson("2.5.6.7", "RFC 4519"),
- /** */
- organizationalRole("2.5.6.8", "RFC 4519"),
- /** */
- groupOfNames("2.5.6.9", "RFC 4519"),
- /** */
- residentialPerson("2.5.6.10", "RFC 4519"),
- /** */
- applicationProcess("2.5.6.11", "RFC 4519"),
- /** */
- device("2.5.6.14", "RFC 4519"),
- /** */
- strongAuthenticationUser("2.5.6.15", "RFC 4523"),
- /** */
- certificationAuthority("2.5.6.16", "RFC 4523"),
- // /** Should be certificationAuthority-V2 */
- // certificationAuthority_V2("2.5.6.16.2", "RFC 4523") {
- // },
- /** */
- groupOfUniqueNames("2.5.6.17", "RFC 4519"),
- /** */
- userSecurityInformation("2.5.6.18", "RFC 4523"),
- /** */
- cRLDistributionPoint("2.5.6.19", "RFC 4523"),
- /** */
- pkiUser("2.5.6.21", "RFC 4523"),
- /** */
- pkiCA("2.5.6.22", "RFC 4523"),
- /** */
- deltaCRL("2.5.6.23", "RFC 4523"),
- /** */
- subschema("2.5.20.1", "RFC 4512"),
- /** */
- ldapSubEntry("2.16.840.1.113719.2.142.6.1.1", "draft-ietf-ldup-subentry"),
- /** */
- changeLogEntry("2.16.840.1.113730.3.2.1", "draft-good-ldap-changelog"),
- /** */
- inetOrgPerson("2.16.840.1.113730.3.2.2", "RFC 2798"),
- /** */
- referral("2.16.840.1.113730.3.2.6", "RFC 3296");
-
- private final static String LDAP_ = "ldap:";
- private final String oid, spec;
-
- private LdapObjs(String oid, String spec) {
- this.oid = oid;
- this.spec = spec;
- }
-
- public String getOid() {
- return oid;
- }
-
- public String getSpec() {
- return spec;
- }
-
- public String property() {
- return new StringBuilder(LDAP_).append(name()).toString();
- }
-
-}
+++ /dev/null
-package org.argeo.naming;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.List;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-import javax.naming.InvalidNameException;
-import javax.naming.NamingException;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.naming.directory.BasicAttribute;
-import javax.naming.directory.BasicAttributes;
-import javax.naming.ldap.LdapName;
-import javax.naming.ldap.Rdn;
-
-import org.argeo.osgi.useradmin.UserDirectoryException;
-
-/** Basic LDIF parser. */
-public class LdifParser {
- private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
-
- protected Attributes addAttributes(SortedMap<LdapName, Attributes> res, int lineNumber, LdapName currentDn,
- Attributes currentAttributes) {
- try {
- Rdn nameRdn = currentDn.getRdn(currentDn.size() - 1);
- Attribute nameAttr = currentAttributes.get(nameRdn.getType());
- if (nameAttr == null)
- currentAttributes.put(nameRdn.getType(), nameRdn.getValue());
- else if (!nameAttr.get().equals(nameRdn.getValue()))
- throw new UserDirectoryException(
- "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + currentDn
- + " (shortly before line " + lineNumber + " in LDIF file)");
- Attributes previous = res.put(currentDn, currentAttributes);
- return previous;
- } catch (NamingException e) {
- throw new UserDirectoryException("Cannot add " + currentDn, e);
- }
- }
-
- /** With UTF-8 charset */
- public SortedMap<LdapName, Attributes> read(InputStream in) throws IOException {
- try (Reader reader = new InputStreamReader(in, DEFAULT_CHARSET)) {
- return read(reader);
- } finally {
- try {
- in.close();
- } catch (IOException e) {
- // silent
- }
- }
- }
-
- /** Will close the reader. */
- public SortedMap<LdapName, Attributes> read(Reader reader) throws IOException {
- SortedMap<LdapName, Attributes> res = new TreeMap<LdapName, Attributes>();
- try {
- List<String> lines = new ArrayList<>();
- try (BufferedReader br = new BufferedReader(reader)) {
- String line;
- while ((line = br.readLine()) != null) {
- lines.add(line);
- }
- }
- if (lines.size() == 0)
- return res;
- // add an empty new line since the last line is not checked
- if (!lines.get(lines.size() - 1).equals(""))
- lines.add("");
-
- LdapName currentDn = null;
- Attributes currentAttributes = null;
- StringBuilder currentEntry = new StringBuilder();
-
- readLines: for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) {
- String line = lines.get(lineNumber);
- boolean isLastLine = false;
- if (lineNumber == lines.size() - 1)
- isLastLine = true;
- if (line.startsWith(" ")) {
- currentEntry.append(line.substring(1));
- if (!isLastLine)
- continue readLines;
- }
-
- if (currentEntry.length() != 0 || isLastLine) {
- // read previous attribute
- StringBuilder attrId = new StringBuilder(8);
- boolean isBase64 = false;
- readAttrId: for (int i = 0; i < currentEntry.length(); i++) {
- char c = currentEntry.charAt(i);
- if (c == ':') {
- if (i + 1 < currentEntry.length() && currentEntry.charAt(i + 1) == ':')
- isBase64 = true;
- currentEntry.delete(0, i + (isBase64 ? 2 : 1));
- break readAttrId;
- } else {
- attrId.append(c);
- }
- }
-
- String attributeId = attrId.toString();
- // TODO should we really trim the end of the string as well?
- String cleanValueStr = currentEntry.toString().trim();
- Object attributeValue = isBase64 ? Base64.getDecoder().decode(cleanValueStr) : cleanValueStr;
-
- // manage DN attributes
- if (attributeId.equals(LdapAttrs.DN) || isLastLine) {
- if (currentDn != null) {
- //
- // ADD
- //
- Attributes previous = addAttributes(res, lineNumber, currentDn, currentAttributes);
- if (previous != null) {
-// log.warn("There was already an entry with DN " + currentDn
-// + ", which has been discarded by a subsequent one.");
- }
- }
-
- if (attributeId.equals(LdapAttrs.DN))
- try {
- currentDn = new LdapName(attributeValue.toString());
- currentAttributes = new BasicAttributes(true);
- } catch (InvalidNameException e) {
-// log.error(attributeValue + " not a valid DN, skipping the entry.");
- currentDn = null;
- currentAttributes = null;
- }
- }
-
- // store attribute
- if (currentAttributes != null) {
- Attribute attribute = currentAttributes.get(attributeId);
- if (attribute == null) {
- attribute = new BasicAttribute(attributeId);
- currentAttributes.put(attribute);
- }
- attribute.add(attributeValue);
- }
- currentEntry = new StringBuilder();
- }
- currentEntry.append(line);
- }
- } finally {
- try {
- reader.close();
- } catch (IOException e) {
- // silent
- }
- }
- return res;
- }
-}
\ No newline at end of file
+++ /dev/null
-package org.argeo.naming;
-
-import static org.argeo.naming.LdapAttrs.DN;
-import static org.argeo.naming.LdapAttrs.member;
-import static org.argeo.naming.LdapAttrs.objectClass;
-import static org.argeo.naming.LdapAttrs.uniqueMember;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-import java.util.Map;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-import javax.naming.NamingEnumeration;
-import javax.naming.NamingException;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapName;
-import javax.naming.ldap.Rdn;
-
-import org.argeo.osgi.useradmin.UserDirectoryException;
-
-/** Basic LDIF writer */
-public class LdifWriter {
- private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
- private final Writer writer;
-
- /** Writer must be closed by caller */
- public LdifWriter(Writer writer) {
- this.writer = writer;
- }
-
- /** Stream must be closed by caller */
- public LdifWriter(OutputStream out) {
- this(new OutputStreamWriter(out, DEFAULT_CHARSET));
- }
-
- public void writeEntry(LdapName name, Attributes attributes) throws IOException {
- try {
- // check consistency
- Rdn nameRdn = name.getRdn(name.size() - 1);
- Attribute nameAttr = attributes.get(nameRdn.getType());
- if (!nameAttr.get().equals(nameRdn.getValue()))
- throw new UserDirectoryException(
- "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + name);
-
- writer.append(DN + ": ").append(name.toString()).append('\n');
- Attribute objectClassAttr = attributes.get(objectClass.name());
- if (objectClassAttr != null)
- writeAttribute(objectClassAttr);
- attributes: for (NamingEnumeration<? extends Attribute> attrs = attributes.getAll(); attrs.hasMore();) {
- Attribute attribute = attrs.next();
- if (attribute.getID().equals(DN) || attribute.getID().equals(objectClass.name()))
- continue attributes;// skip DN attribute
- if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name()))
- continue attributes;// skip member and uniqueMember attributes, so that they are always written last
- writeAttribute(attribute);
- }
- // write member and uniqueMember attributes last
- for (NamingEnumeration<? extends Attribute> attrs = attributes.getAll(); attrs.hasMore();) {
- Attribute attribute = attrs.next();
- if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name()))
- writeMemberAttribute(attribute);
- }
- writer.append('\n');
- writer.flush();
- } catch (NamingException e) {
- throw new UserDirectoryException("Cannot write LDIF", e);
- }
- }
-
- public void write(Map<LdapName, Attributes> entries) throws IOException {
- for (LdapName dn : entries.keySet())
- writeEntry(dn, entries.get(dn));
- }
-
- protected void writeAttribute(Attribute attribute) throws NamingException, IOException {
- for (NamingEnumeration<?> attrValues = attribute.getAll(); attrValues.hasMore();) {
- Object value = attrValues.next();
- if (value instanceof byte[]) {
- String encoded = Base64.getEncoder().encodeToString((byte[]) value);
- writer.append(attribute.getID()).append(":: ").append(encoded).append('\n');
- } else {
- writer.append(attribute.getID()).append(": ").append(value.toString()).append('\n');
- }
- }
- }
-
- protected void writeMemberAttribute(Attribute attribute) throws NamingException, IOException {
- // Note: duplicate entries will be swallowed
- SortedSet<String> values = new TreeSet<>();
- for (NamingEnumeration<?> attrValues = attribute.getAll(); attrValues.hasMore();) {
- String value = attrValues.next().toString();
- values.add(value);
- }
-
- for (String value : values) {
- writer.append(attribute.getID()).append(": ").append(value).append('\n');
- }
- }
-}
+++ /dev/null
-package org.argeo.naming;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-import java.time.Instant;
-import java.time.OffsetDateTime;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
-import java.time.temporal.ChronoField;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-
-public class NamingUtils {
- /** As per https://tools.ietf.org/html/rfc4517#section-3.3.13 */
- private final static DateTimeFormatter utcLdapDate = DateTimeFormatter.ofPattern("uuuuMMddHHmmssX")
- .withZone(ZoneOffset.UTC);
-
- /** @return null if not parseable */
- public static Instant ldapDateToInstant(String ldapDate) {
- try {
- return OffsetDateTime.parse(ldapDate, utcLdapDate).toInstant();
- } catch (DateTimeParseException e) {
- return null;
- }
- }
-
- /** @return null if not parseable */
- public static ZonedDateTime ldapDateToZonedDateTime(String ldapDate) {
- try {
- return OffsetDateTime.parse(ldapDate, utcLdapDate).toZonedDateTime();
- } catch (DateTimeParseException e) {
- return null;
- }
- }
-
- public static Calendar ldapDateToCalendar(String ldapDate) {
- OffsetDateTime instant = OffsetDateTime.parse(ldapDate, utcLdapDate);
- GregorianCalendar calendar = new GregorianCalendar();
- calendar.set(Calendar.DAY_OF_MONTH, instant.get(ChronoField.DAY_OF_MONTH));
- calendar.set(Calendar.MONTH, instant.get(ChronoField.MONTH_OF_YEAR));
- calendar.set(Calendar.YEAR, instant.get(ChronoField.YEAR));
- return calendar;
- }
-
- public static String instantToLdapDate(ZonedDateTime instant) {
- return utcLdapDate.format(instant.withZoneSameInstant(ZoneOffset.UTC));
- }
-
- public static String getQueryValue(Map<String, List<String>> query, String key) {
- if (!query.containsKey(key))
- return null;
- List<String> val = query.get(key);
- if (val.size() == 1)
- return val.get(0);
- else
- throw new IllegalArgumentException("There are " + val.size() + " value(s) for " + key);
- }
-
- public static Map<String, List<String>> queryToMap(URI uri) {
- return queryToMap(uri.getQuery());
- }
-
- private static Map<String, List<String>> queryToMap(String queryPart) {
- try {
- final Map<String, List<String>> query_pairs = new LinkedHashMap<String, List<String>>();
- if (queryPart == null)
- return query_pairs;
- final String[] pairs = queryPart.split("&");
- for (String pair : pairs) {
- final int idx = pair.indexOf("=");
- final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name())
- : pair;
- if (!query_pairs.containsKey(key)) {
- query_pairs.put(key, new LinkedList<String>());
- }
- final String value = idx > 0 && pair.length() > idx + 1
- ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name())
- : null;
- query_pairs.get(key).add(value);
- }
- return query_pairs;
- } catch (UnsupportedEncodingException e) {
- throw new IllegalArgumentException("Cannot convert " + queryPart + " to map", e);
- }
- }
-
- private NamingUtils() {
-
- }
-
- public static void main(String args[]) {
- ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC);
- String str = utcLdapDate.format(now);
- System.out.println(str);
- utcLdapDate.parse(str);
- utcLdapDate.parse("19520512000000Z");
- }
-}
+++ /dev/null
-package org.argeo.naming;
-
-import java.time.Instant;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-
-public class SharedSecret extends AuthPassword {
- public final static String X_SHARED_SECRET = "X-SharedSecret";
- private final Instant expiry;
-
- public SharedSecret(String authInfo, String authValue) {
- super(authInfo, authValue);
- expiry = null;
- }
-
- public SharedSecret(AuthPassword authPassword) {
- super(authPassword);
- String authInfo = getAuthInfo();
- if (authInfo.length() == 16) {
- expiry = NamingUtils.ldapDateToInstant(authInfo);
- } else {
- expiry = null;
- }
- }
-
- public SharedSecret(ZonedDateTime expiryTimestamp, String value) {
- super(NamingUtils.instantToLdapDate(expiryTimestamp), value);
- expiry = expiryTimestamp.withZoneSameInstant(ZoneOffset.UTC).toInstant();
- }
-
- public SharedSecret(int hours, String value) {
- this(ZonedDateTime.now().plusHours(hours), value);
- }
-
- @Override
- protected String getExpectedAuthScheme() {
- return X_SHARED_SECRET;
- }
-
- public boolean isExpired() {
- if (expiry == null)
- return false;
- return expiry.isBefore(Instant.now());
- }
-
-}
+++ /dev/null
-package org.argeo.naming;
-
-/**
- * A name which has been specified and for which an id has been defined
- * (typically an OID).
- */
-public interface SpecifiedName {
- /** The name */
- String name();
-
- /** An RFC or the URLof some specification */
- default String getSpec() {
- return null;
- }
-
- /** Typically an OID */
- default String getID() {
- return getClass().getName() + "." + name();
- }
-}
+++ /dev/null
-package org.argeo.naming;
-
-class SrvRecord implements Comparable<SrvRecord> {
- private final Integer priority;
- private final Integer weight;
- private final Integer port;
- private final String hostname;
-
- public SrvRecord(Integer priority, Integer weight, Integer port, String hostname) {
- this.priority = priority;
- this.weight = weight;
- this.port = port;
- this.hostname = hostname;
- }
-
- @Override
- public int compareTo(SrvRecord other) {
- // https: // en.wikipedia.org/wiki/SRV_record
- if (priority != other.priority)
- return priority - other.priority;
- if (weight != other.weight)
- return other.weight - other.weight;
- String host = toHost(false);
- String otherHost = other.toHost(false);
- if (host.length() == otherHost.length())
- return host.compareTo(otherHost);
- else
- return host.length() - otherHost.length();
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof SrvRecord) {
- SrvRecord other = (SrvRecord) obj;
- return priority == other.priority && weight == other.weight && port == other.port
- && hostname.equals(other.hostname);
- }
- return false;
- }
-
- @Override
- public String toString() {
- return priority + " " + weight;
- }
-
- public String toHost(boolean withPort) {
- String hostStr = hostname;
- if (hostname.charAt(hostname.length() - 1) == '.')
- hostStr = hostname.substring(0, hostname.length() - 1);
- return hostStr + (withPort ? ":" + port : "");
- }
-}
+++ /dev/null
-/** Generic naming and LDAP support. */
-package org.argeo.naming;
\ No newline at end of file
+++ /dev/null
-package org.argeo.osgi.internal;
-
-import org.osgi.framework.BundleActivator;
-import org.osgi.framework.BundleContext;
-
-/**
- * Called to gather information about the OSGi runtime. Should not activate
- * anything else that canonical monitoring services (not creating implicit
- * APIs), which is the responsibility of higher levels.
- */
-public class EnterpriseActivator implements BundleActivator {
-
- @Override
- public void start(BundleContext context) throws Exception {
- }
-
- @Override
- public void stop(BundleContext context) throws Exception {
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.metatype;
-
-import org.argeo.naming.SpecifiedName;
-import org.osgi.service.metatype.AttributeDefinition;
-
-public interface EnumAD extends SpecifiedName, AttributeDefinition {
- String name();
-
- default Object getDefault() {
- return null;
- }
-
- @Override
- default String getName() {
- return name();
- }
-
- @Override
- default String getID() {
- return getClass().getName() + "." + name();
- }
-
- @Override
- default String getDescription() {
- return null;
- }
-
- @Override
- default int getCardinality() {
- return 0;
- }
-
- @Override
- default int getType() {
- return STRING;
- }
-
- @Override
- default String[] getOptionValues() {
- return null;
- }
-
- @Override
- default String[] getOptionLabels() {
- return null;
- }
-
- @Override
- default String validate(String value) {
- return null;
- }
-
- @Override
- default String[] getDefaultValue() {
- Object value = getDefault();
- if (value == null)
- return null;
- return new String[] { value.toString() };
- }
-}
+++ /dev/null
-package org.argeo.osgi.metatype;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.EnumSet;
-import java.util.List;
-
-import org.osgi.service.metatype.AttributeDefinition;
-import org.osgi.service.metatype.ObjectClassDefinition;
-
-public class EnumOCD<T extends Enum<T>> implements ObjectClassDefinition {
- private final Class<T> enumClass;
- private String locale;
-
- public EnumOCD(Class<T> clazz, String locale) {
- this.enumClass = clazz;
- this.locale = locale;
- }
-
- @Override
- public String getName() {
- return null;
- }
-
- public String getLocale() {
- return locale;
- }
-
- @Override
- public String getID() {
- return enumClass.getName();
- }
-
- @Override
- public String getDescription() {
- return null;
- }
-
- @Override
- public AttributeDefinition[] getAttributeDefinitions(int filter) {
- EnumSet<T> set = EnumSet.allOf(enumClass);
- List<AttributeDefinition> attrs = new ArrayList<>();
- for (T key : set)
- attrs.add((AttributeDefinition) key);
- return attrs.toArray(new AttributeDefinition[attrs.size()]);
- }
-
- @Override
- public InputStream getIcon(int size) throws IOException {
- return null;
- }
-
-}
+++ /dev/null
-/** OSGi metatype support. */
-package org.argeo.osgi.metatype;
\ No newline at end of file
+++ /dev/null
-package org.argeo.osgi.provisioning;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.TreeMap;
-import java.util.zip.ZipInputStream;
-
-import org.osgi.service.provisioning.ProvisioningService;
-
-public class SimpleProvisioningService implements ProvisioningService {
- private Map<String, Object> map = Collections.synchronizedSortedMap(new TreeMap<>());
-
- public SimpleProvisioningService() {
- // update count
- map.put(PROVISIONING_UPDATE_COUNT, 0);
- }
-
- @Override
- public Dictionary<String, Object> getInformation() {
- return new Information();
- }
-
- @Override
- public synchronized void setInformation(Dictionary<String, ?> info) {
- map.clear();
- addInformation(info);
- }
-
- @Override
- public synchronized void addInformation(Dictionary<String, ?> info) {
- Enumeration<String> e = info.keys();
- while (e.hasMoreElements()) {
- String key = e.nextElement();
- map.put(key, info.get(key));
- }
- incrementProvisioningUpdateCount();
- }
-
- protected synchronized void incrementProvisioningUpdateCount() {
- Integer current = (Integer) map.get(PROVISIONING_UPDATE_COUNT);
- Integer newValue = current + 1;
- map.put(PROVISIONING_UPDATE_COUNT, newValue);
- }
-
- @Override
- public synchronized void addInformation(ZipInputStream zis) throws IOException {
- throw new UnsupportedOperationException();
- }
-
- class Information extends Dictionary<String, Object> {
-
- @Override
- public int size() {
- return map.size();
- }
-
- @Override
- public boolean isEmpty() {
- return map.isEmpty();
- }
-
- @Override
- public Enumeration<String> keys() {
- Iterator<String> it = map.keySet().iterator();
- return new Enumeration<String>() {
-
- @Override
- public boolean hasMoreElements() {
- return it.hasNext();
- }
-
- @Override
- public String nextElement() {
- return it.next();
- }
-
- };
- }
-
- @Override
- public Enumeration<Object> elements() {
- Iterator<Object> it = map.values().iterator();
- return new Enumeration<Object>() {
-
- @Override
- public boolean hasMoreElements() {
- return it.hasNext();
- }
-
- @Override
- public Object nextElement() {
- return it.next();
- }
-
- };
- }
-
- @Override
- public Object get(Object key) {
- return map.get(key);
- }
-
- @Override
- public Object put(String key, Object value) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Object remove(Object key) {
- throw new UnsupportedOperationException();
- }
-
- }
-}
+++ /dev/null
-/** OSGi provisioning support. */
-package org.argeo.osgi.provisioning;
\ No newline at end of file
+++ /dev/null
-package org.argeo.osgi.transaction;
-
-/** JTA transaction status. */
-public class JtaStatusAdapter implements TransactionStatusAdapter<Integer> {
- private static final Integer STATUS_ACTIVE = 0;
- private static final Integer STATUS_COMMITTED = 3;
- private static final Integer STATUS_COMMITTING = 8;
- private static final Integer STATUS_MARKED_ROLLBACK = 1;
- private static final Integer STATUS_NO_TRANSACTION = 6;
- private static final Integer STATUS_PREPARED = 2;
- private static final Integer STATUS_PREPARING = 7;
- private static final Integer STATUS_ROLLEDBACK = 4;
- private static final Integer STATUS_ROLLING_BACK = 9;
-// private static final Integer STATUS_UNKNOWN = 5;
-
- @Override
- public Integer getActiveStatus() {
- return STATUS_ACTIVE;
- }
-
- @Override
- public Integer getPreparingStatus() {
- return STATUS_PREPARING;
- }
-
- @Override
- public Integer getMarkedRollbackStatus() {
- return STATUS_MARKED_ROLLBACK;
- }
-
- @Override
- public Integer getPreparedStatus() {
- return STATUS_PREPARED;
- }
-
- @Override
- public Integer getCommittingStatus() {
- return STATUS_COMMITTING;
- }
-
- @Override
- public Integer getCommittedStatus() {
- return STATUS_COMMITTED;
- }
-
- @Override
- public Integer getRollingBackStatus() {
- return STATUS_ROLLING_BACK;
- }
-
- @Override
- public Integer getRolledBackStatus() {
- return STATUS_ROLLEDBACK;
- }
-
- @Override
- public Integer getNoTransactionStatus() {
- return STATUS_NO_TRANSACTION;
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.transaction;
-
-/** Internal unchecked rollback exception. */
-class SimpleRollbackException extends RuntimeException {
- private static final long serialVersionUID = 8055601819719780566L;
-
- public SimpleRollbackException() {
- super();
- }
-
- public SimpleRollbackException(Throwable cause) {
- super(cause);
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.transaction;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.transaction.xa.XAException;
-import javax.transaction.xa.XAResource;
-import javax.transaction.xa.Xid;
-
-/** Simple implementation of an XA transaction. */
-class SimpleTransaction<T>
-//implements Transaction, Status
-{
- private final Xid xid;
- private T status;
- private final List<XAResource> xaResources = new ArrayList<XAResource>();
-
- private final SimpleTransactionManager transactionManager;
- private TransactionStatusAdapter<T> tsa;
-
- public SimpleTransaction(SimpleTransactionManager transactionManager, TransactionStatusAdapter<T> tsa) {
- this.tsa = tsa;
- this.status = tsa.getActiveStatus();
- this.xid = new UuidXid();
- this.transactionManager = transactionManager;
- }
-
- public synchronized void commit()
-// throws RollbackException, HeuristicMixedException, HeuristicRollbackException,
-// SecurityException, IllegalStateException, SystemException
- {
- status = tsa.getPreparingStatus();
- for (XAResource xaRes : xaResources) {
- if (status.equals(tsa.getMarkedRollbackStatus()))
- break;
- try {
- xaRes.prepare(xid);
- } catch (XAException e) {
- status = tsa.getMarkedRollbackStatus();
- error("Cannot prepare " + xaRes + " for " + xid, e);
- }
- }
- if (status.equals(tsa.getMarkedRollbackStatus())) {
- rollback();
- throw new SimpleRollbackException();
- }
- status = tsa.getPreparedStatus();
-
- status = tsa.getCommittingStatus();
- for (XAResource xaRes : xaResources) {
- if (status.equals(tsa.getMarkedRollbackStatus()))
- break;
- try {
- xaRes.commit(xid, false);
- } catch (XAException e) {
- status = tsa.getMarkedRollbackStatus();
- error("Cannot prepare " + xaRes + " for " + xid, e);
- }
- }
- if (status.equals(tsa.getMarkedRollbackStatus())) {
- rollback();
- throw new SimpleRollbackException();
- }
-
- // complete
- status = tsa.getCommittedStatus();
- clearResources(XAResource.TMSUCCESS);
- transactionManager.unregister(xid);
- }
-
- public synchronized void rollback()
-// throws IllegalStateException, SystemException
- {
- status = tsa.getRollingBackStatus();
- for (XAResource xaRes : xaResources) {
- try {
- xaRes.rollback(xid);
- } catch (XAException e) {
- error("Cannot rollback " + xaRes + " for " + xid, e);
- }
- }
-
- // complete
- status = tsa.getRolledBackStatus();
- clearResources(XAResource.TMFAIL);
- transactionManager.unregister(xid);
- }
-
- public synchronized boolean enlistResource(XAResource xaRes)
-// throws RollbackException, IllegalStateException, SystemException
- {
- if (xaResources.add(xaRes)) {
- try {
- xaRes.start(getXid(), XAResource.TMNOFLAGS);
- return true;
- } catch (XAException e) {
- error("Cannot enlist " + xaRes, e);
- return false;
- }
- } else
- return false;
- }
-
- public synchronized boolean delistResource(XAResource xaRes, int flag)
-// throws IllegalStateException, SystemException
- {
- if (xaResources.remove(xaRes)) {
- try {
- xaRes.end(getXid(), flag);
- } catch (XAException e) {
- error("Cannot delist " + xaRes, e);
- return false;
- }
- return true;
- } else
- return false;
- }
-
- protected void clearResources(int flag) {
- for (XAResource xaRes : xaResources)
- try {
- xaRes.end(getXid(), flag);
- } catch (XAException e) {
- error("Cannot end " + xaRes, e);
- }
- xaResources.clear();
- }
-
- protected void error(Object obj, Exception e) {
- System.err.println(obj);
- e.printStackTrace();
- }
-
- public synchronized T getStatus()
-// throws SystemException
- {
- return status;
- }
-
-// public void registerSynchronization(Synchronization sync)
-// throws RollbackException, IllegalStateException, SystemException {
-// throw new UnsupportedOperationException();
-// }
-
- public void setRollbackOnly()
-// throws IllegalStateException, SystemException
- {
- status = tsa.getMarkedRollbackStatus();
- }
-
- @Override
- public int hashCode() {
- return xid.hashCode();
- }
-
- Xid getXid() {
- return xid;
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.transaction;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.Callable;
-
-import javax.transaction.xa.XAResource;
-import javax.transaction.xa.Xid;
-
-/**
- * Simple implementation of an XA transaction manager.
- */
-public class SimpleTransactionManager
-// implements TransactionManager, UserTransaction
- implements WorkControl, WorkTransaction {
- private ThreadLocal<SimpleTransaction<Integer>> current = new ThreadLocal<SimpleTransaction<Integer>>();
-
- private Map<Xid, SimpleTransaction<Integer>> knownTransactions = Collections
- .synchronizedMap(new HashMap<Xid, SimpleTransaction<Integer>>());
- private TransactionStatusAdapter<Integer> tsa = new JtaStatusAdapter();
-// private SyncRegistry syncRegistry = new SyncRegistry();
-
- /*
- * WORK IMPLEMENTATION
- */
- @Override
- public <T> T required(Callable<T> work) {
- T res;
- begin();
- try {
- res = work.call();
- commit();
- } catch (Exception e) {
- rollback();
- throw new SimpleRollbackException(e);
- }
- return res;
- }
-
- @Override
- public WorkContext getWorkContext() {
- return new WorkContext() {
-
- @Override
- public void registerXAResource(XAResource resource, String recoveryId) {
- getTransaction().enlistResource(resource);
- }
- };
- }
-
- /*
- * WORK TRANSACTION IMPLEMENTATION
- */
-
- @Override
- public boolean isNoTransactionStatus() {
- return tsa.getNoTransactionStatus().equals(getStatus());
- }
-
- /*
- * JTA IMPLEMENTATION
- */
-
- public void begin()
-// throws NotSupportedException, SystemException
- {
- if (getCurrent() != null)
- throw new UnsupportedOperationException("Nested transactions are not supported");
- SimpleTransaction<Integer> transaction = new SimpleTransaction<Integer>(this, tsa);
- knownTransactions.put(transaction.getXid(), transaction);
- current.set(transaction);
- }
-
- public void commit()
-// throws RollbackException, HeuristicMixedException, HeuristicRollbackException,
-// SecurityException, IllegalStateException, SystemException
- {
- if (getCurrent() == null)
- throw new IllegalStateException("No transaction registered with the current thread.");
- getCurrent().commit();
- }
-
- public int getStatus()
-// throws SystemException
- {
- if (getCurrent() == null)
- return tsa.getNoTransactionStatus();
- return getTransaction().getStatus();
- }
-
- public SimpleTransaction<Integer> getTransaction()
-// throws SystemException
- {
- return getCurrent();
- }
-
- protected SimpleTransaction<Integer> getCurrent()
-// throws SystemException
- {
- SimpleTransaction<Integer> transaction = current.get();
- if (transaction == null)
- return null;
- Integer status = transaction.getStatus();
- if (status.equals(tsa.getCommittedStatus()) || status.equals(tsa.getRolledBackStatus())) {
- current.remove();
- return null;
- }
- return transaction;
- }
-
- void unregister(Xid xid) {
- knownTransactions.remove(xid);
- }
-
- public void resume(SimpleTransaction<Integer> tobj)
-// throws InvalidTransactionException, IllegalStateException, SystemException
- {
- if (getCurrent() != null)
- throw new IllegalStateException("Transaction " + current.get() + " already registered");
- current.set(tobj);
- }
-
- public void rollback()
-// throws IllegalStateException, SecurityException, SystemException
- {
- if (getCurrent() == null)
- throw new IllegalStateException("No transaction registered with the current thread.");
- getCurrent().rollback();
- }
-
- public void setRollbackOnly()
-// throws IllegalStateException, SystemException
- {
- if (getCurrent() == null)
- throw new IllegalStateException("No transaction registered with the current thread.");
- getCurrent().setRollbackOnly();
- }
-
- public void setTransactionTimeout(int seconds)
-// throws SystemException
- {
- throw new UnsupportedOperationException();
- }
-
- public SimpleTransaction<Integer> suspend()
-// throws SystemException
- {
- SimpleTransaction<Integer> transaction = getCurrent();
- current.remove();
- return transaction;
- }
-
-// public TransactionSynchronizationRegistry getTsr() {
-// return syncRegistry;
-// }
-//
-// private class SyncRegistry implements TransactionSynchronizationRegistry {
-// @Override
-// public Object getTransactionKey() {
-// try {
-// SimpleTransaction transaction = getCurrent();
-// if (transaction == null)
-// return null;
-// return getCurrent().getXid();
-// } catch (SystemException e) {
-// throw new IllegalStateException("Cannot get transaction key", e);
-// }
-// }
-//
-// @Override
-// public void putResource(Object key, Object value) {
-// throw new UnsupportedOperationException();
-// }
-//
-// @Override
-// public Object getResource(Object key) {
-// throw new UnsupportedOperationException();
-// }
-//
-// @Override
-// public void registerInterposedSynchronization(Synchronization sync) {
-// throw new UnsupportedOperationException();
-// }
-//
-// @Override
-// public int getTransactionStatus() {
-// try {
-// return getStatus();
-// } catch (SystemException e) {
-// throw new IllegalStateException("Cannot get status", e);
-// }
-// }
-//
-// @Override
-// public boolean getRollbackOnly() {
-// try {
-// return getStatus() == Status.STATUS_MARKED_ROLLBACK;
-// } catch (SystemException e) {
-// throw new IllegalStateException("Cannot get status", e);
-// }
-// }
-//
-// @Override
-// public void setRollbackOnly() {
-// try {
-// getCurrent().setRollbackOnly();
-// } catch (Exception e) {
-// throw new IllegalStateException("Cannot set rollback only", e);
-// }
-// }
-//
-// }
-}
+++ /dev/null
-package org.argeo.osgi.transaction;
-
-/** Abstract the various approaches to represent transaction status. */
-public interface TransactionStatusAdapter<T> {
- T getActiveStatus();
-
- T getPreparingStatus();
-
- T getMarkedRollbackStatus();
-
- T getPreparedStatus();
-
- T getCommittingStatus();
-
- T getCommittedStatus();
-
- T getRollingBackStatus();
-
- T getRolledBackStatus();
-
- T getNoTransactionStatus();
-}
+++ /dev/null
-package org.argeo.osgi.transaction;
-
-import java.io.Serializable;
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.UUID;
-
-import javax.transaction.xa.Xid;
-
-/**
- * Implementation of {@link Xid} based on {@link UUID}, using max significant
- * bits as global transaction id, and least significant bits as branch
- * qualifier.
- */
-public class UuidXid implements Xid, Serializable {
- private static final long serialVersionUID = -5380531989917886819L;
- public final static int FORMAT = (int) serialVersionUID;
-
- private final static int BYTES_PER_LONG = Long.SIZE / Byte.SIZE;
-
- private final int format;
- private final byte[] globalTransactionId;
- private final byte[] branchQualifier;
- private final String uuid;
- private final int hashCode;
-
- public UuidXid() {
- this(UUID.randomUUID());
- }
-
- public UuidXid(UUID uuid) {
- this.format = FORMAT;
- this.globalTransactionId = uuidToBytes(uuid.getMostSignificantBits());
- this.branchQualifier = uuidToBytes(uuid.getLeastSignificantBits());
- this.uuid = uuid.toString();
- this.hashCode = uuid.hashCode();
- }
-
- public UuidXid(Xid xid) {
- this(xid.getFormatId(), xid.getGlobalTransactionId(), xid
- .getBranchQualifier());
- }
-
- private UuidXid(int format, byte[] globalTransactionId,
- byte[] branchQualifier) {
- this.format = format;
- this.globalTransactionId = globalTransactionId;
- this.branchQualifier = branchQualifier;
- this.uuid = bytesToUUID(globalTransactionId, branchQualifier)
- .toString();
- this.hashCode = uuid.hashCode();
- }
-
- @Override
- public int getFormatId() {
- return format;
- }
-
- @Override
- public byte[] getGlobalTransactionId() {
- return Arrays.copyOf(globalTransactionId, globalTransactionId.length);
- }
-
- @Override
- public byte[] getBranchQualifier() {
- return Arrays.copyOf(branchQualifier, branchQualifier.length);
- }
-
- @Override
- public int hashCode() {
- return hashCode;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj instanceof UuidXid) {
- UuidXid that = (UuidXid) obj;
- return Arrays.equals(globalTransactionId, that.globalTransactionId)
- && Arrays.equals(branchQualifier, that.branchQualifier);
- }
- if (obj instanceof Xid) {
- Xid that = (Xid) obj;
- return Arrays.equals(globalTransactionId,
- that.getGlobalTransactionId())
- && Arrays
- .equals(branchQualifier, that.getBranchQualifier());
- }
- return uuid.equals(obj.toString());
- }
-
- @Override
- protected Object clone() throws CloneNotSupportedException {
- return new UuidXid(format, globalTransactionId, branchQualifier);
- }
-
- @Override
- public String toString() {
- return uuid;
- }
-
- public UUID asUuid() {
- return bytesToUUID(globalTransactionId, branchQualifier);
- }
-
- public static byte[] uuidToBytes(long bits) {
- ByteBuffer buffer = ByteBuffer.allocate(BYTES_PER_LONG);
- buffer.putLong(0, bits);
- return buffer.array();
- }
-
- public static UUID bytesToUUID(byte[] most, byte[] least) {
- if (most.length < BYTES_PER_LONG)
- most = Arrays.copyOf(most, BYTES_PER_LONG);
- if (least.length < BYTES_PER_LONG)
- least = Arrays.copyOf(least, BYTES_PER_LONG);
- ByteBuffer buffer = ByteBuffer.allocate(2 * BYTES_PER_LONG);
- buffer.put(most, 0, BYTES_PER_LONG);
- buffer.put(least, 0, BYTES_PER_LONG);
- buffer.flip();
- return new UUID(buffer.getLong(), buffer.getLong());
- }
-
- // public static void main(String[] args) {
- // UUID uuid = UUID.randomUUID();
- // System.out.println(uuid);
- // uuid = bytesToUUID(uuidToBytes(uuid.getMostSignificantBits()),
- // uuidToBytes(uuid.getLeastSignificantBits()));
- // System.out.println(uuid);
- // }
-}
+++ /dev/null
-package org.argeo.osgi.transaction;
-
-import javax.transaction.xa.XAResource;
-
-/**
- * A minimalistic interface similar to OSGi transaction context in order to
- * register XA resources.
- */
-public interface WorkContext {
- void registerXAResource(XAResource resource, String recoveryId);
-}
+++ /dev/null
-package org.argeo.osgi.transaction;
-
-import java.util.concurrent.Callable;
-
-/**
- * A minimalistic interface inspired by OSGi transaction control in order to
- * commit units of work externally.
- */
-public interface WorkControl {
- <T> T required(Callable<T> work);
-
- void setRollbackOnly();
-
- WorkContext getWorkContext();
-}
+++ /dev/null
-package org.argeo.osgi.transaction;
-
-/**
- * A minimalistic interface inspired by JTA user transaction in order to commit
- * units of work externally.
- */
-public interface WorkTransaction {
- void begin();
-
- void commit();
-
- void rollback();
-
- boolean isNoTransactionStatus();
-}
+++ /dev/null
-/** Minimalistic and partial XA transaction manager implementation. */
-package org.argeo.osgi.transaction;
\ No newline at end of file
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import static org.argeo.naming.LdapAttrs.objectClass;
-import static org.argeo.naming.LdapObjs.extensibleObject;
-import static org.argeo.naming.LdapObjs.inetOrgPerson;
-import static org.argeo.naming.LdapObjs.organizationalPerson;
-import static org.argeo.naming.LdapObjs.person;
-import static org.argeo.naming.LdapObjs.top;
-
-import java.io.File;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.Hashtable;
-import java.util.Iterator;
-import java.util.List;
-
-import javax.naming.InvalidNameException;
-import javax.naming.NameNotFoundException;
-import javax.naming.NamingEnumeration;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.naming.directory.BasicAttribute;
-import javax.naming.directory.BasicAttributes;
-import javax.naming.ldap.LdapName;
-import javax.naming.ldap.Rdn;
-
-import org.argeo.naming.LdapAttrs;
-import org.argeo.osgi.transaction.WorkControl;
-import org.osgi.framework.Filter;
-import org.osgi.framework.FrameworkUtil;
-import org.osgi.framework.InvalidSyntaxException;
-import org.osgi.service.useradmin.Authorization;
-import org.osgi.service.useradmin.Role;
-import org.osgi.service.useradmin.User;
-import org.osgi.service.useradmin.UserAdmin;
-
-/** Base class for a {@link UserDirectory}. */
-public abstract class AbstractUserDirectory implements UserAdmin, UserDirectory {
- static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name";
- static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password";
-
- private final Hashtable<String, Object> properties;
- private final LdapName baseDn, userBaseDn, groupBaseDn;
- private final String userObjectClass, userBase, groupObjectClass, groupBase;
-
- private final boolean readOnly;
- private final boolean disabled;
- private final String uri;
-
- private UserAdmin externalRoles;
- // private List<String> indexedUserProperties = Arrays
- // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(),
- // LdapAttrs.cn.name() });
-
- private final boolean scoped;
-
- private String memberAttributeId = "member";
- private List<String> credentialAttributeIds = Arrays
- .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
-
- // Transaction
-// private TransactionManager transactionManager;
- private WorkControl transactionControl;
- private WcXaResource xaResource = new WcXaResource(this);
-
- AbstractUserDirectory(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
- this.scoped = scoped;
- properties = new Hashtable<String, Object>();
- for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
- String key = keys.nextElement();
- properties.put(key, props.get(key));
- }
-
- if (uriArg != null) {
- uri = uriArg.toString();
- // uri from properties is ignored
- } else {
- String uriStr = UserAdminConf.uri.getValue(properties);
- if (uriStr == null)
- uri = null;
- else
- uri = uriStr;
- }
-
- userObjectClass = UserAdminConf.userObjectClass.getValue(properties);
- userBase = UserAdminConf.userBase.getValue(properties);
- groupObjectClass = UserAdminConf.groupObjectClass.getValue(properties);
- groupBase = UserAdminConf.groupBase.getValue(properties);
- try {
- baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
- userBaseDn = new LdapName(userBase + "," + baseDn);
- groupBaseDn = new LdapName(groupBase + "," + baseDn);
- } catch (InvalidNameException e) {
- throw new UserDirectoryException("Badly formated base DN " + UserAdminConf.baseDn.getValue(properties), e);
- }
- String readOnlyStr = UserAdminConf.readOnly.getValue(properties);
- if (readOnlyStr == null) {
- readOnly = readOnlyDefault(uri);
- properties.put(UserAdminConf.readOnly.name(), Boolean.toString(readOnly));
- } else
- readOnly = Boolean.parseBoolean(readOnlyStr);
- String disabledStr = UserAdminConf.disabled.getValue(properties);
- if (disabledStr != null)
- disabled = Boolean.parseBoolean(disabledStr);
- else
- disabled = false;
- }
-
- /** Returns the groups this user is a direct member of. */
- protected abstract List<LdapName> getDirectGroups(LdapName dn);
-
- protected abstract Boolean daoHasRole(LdapName dn);
-
- protected abstract DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException;
-
- protected abstract List<DirectoryUser> doGetRoles(Filter f);
-
- protected abstract AbstractUserDirectory scope(User user);
-
- public void init() {
-
- }
-
- public void destroy() {
-
- }
-
- protected boolean isEditing() {
- return xaResource.wc() != null;
- }
-
- protected UserDirectoryWorkingCopy getWorkingCopy() {
- UserDirectoryWorkingCopy wc = xaResource.wc();
- if (wc == null)
- return null;
- return wc;
- }
-
- protected void checkEdit() {
-// Transaction transaction;
-// try {
-// transaction = transactionManager.getTransaction();
-// } catch (SystemException e) {
-// throw new UserDirectoryException("Cannot get transaction", e);
-// }
-// if (transaction == null)
-// throw new UserDirectoryException("A transaction needs to be active in order to edit");
- if (xaResource.wc() == null) {
- try {
-// transaction.enlistResource(xaResource);
- transactionControl.getWorkContext().registerXAResource(xaResource, null);
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot enlist " + xaResource, e);
- }
- } else {
- }
- }
-
- protected List<Role> getAllRoles(DirectoryUser user) {
- List<Role> allRoles = new ArrayList<Role>();
- if (user != null) {
- collectRoles(user, allRoles);
- allRoles.add(user);
- } else
- collectAnonymousRoles(allRoles);
- return allRoles;
- }
-
- private void collectRoles(DirectoryUser user, List<Role> allRoles) {
- Attributes attrs = user.getAttributes();
- // TODO centralize attribute name
- Attribute memberOf = attrs.get(LdapAttrs.memberOf.name());
- // if user belongs to this directory, we only check meberOf
- if (memberOf != null && user.getDn().startsWith(getBaseDn())) {
- try {
- NamingEnumeration<?> values = memberOf.getAll();
- while (values.hasMore()) {
- Object value = values.next();
- LdapName groupDn = new LdapName(value.toString());
- DirectoryUser group = doGetRole(groupDn);
- if (group != null)
- allRoles.add(group);
- }
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot get memberOf groups for " + user, e);
- }
- } else {
- for (LdapName groupDn : getDirectGroups(user.getDn())) {
- // TODO check for loops
- DirectoryUser group = doGetRole(groupDn);
- if (group != null) {
- allRoles.add(group);
- collectRoles(group, allRoles);
- }
- }
- }
- }
-
- private void collectAnonymousRoles(List<Role> allRoles) {
- // TODO gather anonymous roles
- }
-
- // USER ADMIN
- @Override
- public Role getRole(String name) {
- return doGetRole(toDn(name));
- }
-
- protected DirectoryUser doGetRole(LdapName dn) {
- UserDirectoryWorkingCopy wc = getWorkingCopy();
- DirectoryUser user;
- try {
- user = daoGetRole(dn);
- } catch (NameNotFoundException e) {
- user = null;
- }
- if (wc != null) {
- if (user == null && wc.getNewUsers().containsKey(dn))
- user = wc.getNewUsers().get(dn);
- else if (wc.getDeletedUsers().containsKey(dn))
- user = null;
- }
- return user;
- }
-
- @Override
- public Role[] getRoles(String filter) throws InvalidSyntaxException {
- UserDirectoryWorkingCopy wc = getWorkingCopy();
- Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
- List<DirectoryUser> res = doGetRoles(f);
- if (wc != null) {
- for (Iterator<DirectoryUser> it = res.iterator(); it.hasNext();) {
- DirectoryUser user = it.next();
- LdapName dn = user.getDn();
- if (wc.getDeletedUsers().containsKey(dn))
- it.remove();
- }
- for (DirectoryUser user : wc.getNewUsers().values()) {
- if (f == null || f.match(user.getProperties()))
- res.add(user);
- }
- // no need to check modified users,
- // since doGetRoles was already based on the modified attributes
- }
- return res.toArray(new Role[res.size()]);
- }
-
- @Override
- public User getUser(String key, String value) {
- // TODO check value null or empty
- List<DirectoryUser> collectedUsers = new ArrayList<DirectoryUser>();
- if (key != null) {
- doGetUser(key, value, collectedUsers);
- } else {
- throw new UserDirectoryException("Key cannot be null");
- }
-
- if (collectedUsers.size() == 1) {
- return collectedUsers.get(0);
- } else if (collectedUsers.size() > 1) {
- // log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" :
- // "") + value);
- }
- return null;
- }
-
- protected void doGetUser(String key, String value, List<DirectoryUser> collectedUsers) {
- try {
- Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")");
- List<DirectoryUser> users = doGetRoles(f);
- collectedUsers.addAll(users);
- } catch (InvalidSyntaxException e) {
- throw new UserDirectoryException("Cannot get user with " + key + "=" + value, e);
- }
- }
-
- @Override
- public Authorization getAuthorization(User user) {
- if (user == null || user instanceof DirectoryUser) {
- return new LdifAuthorization(user, getAllRoles((DirectoryUser) user));
- } else {
- // bind
- AbstractUserDirectory scopedUserAdmin = scope(user);
- try {
- DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName());
- if (directoryUser == null)
- throw new UserDirectoryException("No scoped user found for " + user);
- LdifAuthorization authorization = new LdifAuthorization(directoryUser,
- scopedUserAdmin.getAllRoles(directoryUser));
- return authorization;
- } finally {
- scopedUserAdmin.destroy();
- }
- }
- }
-
- @Override
- public Role createRole(String name, int type) {
- checkEdit();
- UserDirectoryWorkingCopy wc = getWorkingCopy();
- LdapName dn = toDn(name);
- if ((daoHasRole(dn) && !wc.getDeletedUsers().containsKey(dn)) || wc.getNewUsers().containsKey(dn))
- throw new UserDirectoryException("Already a role " + name);
- BasicAttributes attrs = new BasicAttributes(true);
- // attrs.put(LdifName.dn.name(), dn.toString());
- Rdn nameRdn = dn.getRdn(dn.size() - 1);
- // TODO deal with multiple attr RDN
- attrs.put(nameRdn.getType(), nameRdn.getValue());
- if (wc.getDeletedUsers().containsKey(dn)) {
- wc.getDeletedUsers().remove(dn);
- wc.getModifiedUsers().put(dn, attrs);
- return getRole(name);
- } else {
- wc.getModifiedUsers().put(dn, attrs);
- DirectoryUser newRole = newRole(dn, type, attrs);
- wc.getNewUsers().put(dn, newRole);
- return newRole;
- }
- }
-
- protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) {
- LdifUser newRole;
- BasicAttribute objClass = new BasicAttribute(objectClass.name());
- if (type == Role.USER) {
- String userObjClass = newUserObjectClass(dn);
- objClass.add(userObjClass);
- if (inetOrgPerson.name().equals(userObjClass)) {
- objClass.add(organizationalPerson.name());
- objClass.add(person.name());
- } else if (organizationalPerson.name().equals(userObjClass)) {
- objClass.add(person.name());
- }
- objClass.add(top.name());
- objClass.add(extensibleObject.name());
- attrs.put(objClass);
- newRole = new LdifUser(this, dn, attrs);
- } else if (type == Role.GROUP) {
- String groupObjClass = getGroupObjectClass();
- objClass.add(groupObjClass);
- // objClass.add(LdifName.extensibleObject.name());
- objClass.add(top.name());
- attrs.put(objClass);
- newRole = new LdifGroup(this, dn, attrs);
- } else
- throw new UserDirectoryException("Unsupported type " + type);
- return newRole;
- }
-
- @Override
- public boolean removeRole(String name) {
- checkEdit();
- UserDirectoryWorkingCopy wc = getWorkingCopy();
- LdapName dn = toDn(name);
- boolean actuallyDeleted;
- if (daoHasRole(dn) || wc.getNewUsers().containsKey(dn)) {
- DirectoryUser user = (DirectoryUser) getRole(name);
- wc.getDeletedUsers().put(dn, user);
- actuallyDeleted = true;
- } else {// just removing from groups (e.g. system roles)
- actuallyDeleted = false;
- }
- for (LdapName groupDn : getDirectGroups(dn)) {
- DirectoryUser group = doGetRole(groupDn);
- group.getAttributes().get(getMemberAttributeId()).remove(dn.toString());
- }
- return actuallyDeleted;
- }
-
- // TRANSACTION
- protected void prepare(UserDirectoryWorkingCopy wc) {
-
- }
-
- protected void commit(UserDirectoryWorkingCopy wc) {
-
- }
-
- protected void rollback(UserDirectoryWorkingCopy wc) {
-
- }
-
- // UTILITIES
- protected LdapName toDn(String name) {
- try {
- return new LdapName(name);
- } catch (InvalidNameException e) {
- throw new UserDirectoryException("Badly formatted name", e);
- }
- }
-
- // GETTERS
- protected String getMemberAttributeId() {
- return memberAttributeId;
- }
-
- protected List<String> getCredentialAttributeIds() {
- return credentialAttributeIds;
- }
-
- protected String getUri() {
- return uri;
- }
-
- private static boolean readOnlyDefault(String uriStr) {
- if (uriStr == null)
- return true;
- /// TODO make it more generic
- URI uri;
- try {
- uri = new URI(uriStr.split(" ")[0]);
- } catch (URISyntaxException e) {
- throw new IllegalArgumentException(e);
- }
- if (uri.getScheme() == null)
- return false;// assume relative file to be writable
- if (uri.getScheme().equals(UserAdminConf.SCHEME_FILE)) {
- File file = new File(uri);
- if (file.exists())
- return !file.canWrite();
- else
- return !file.getParentFile().canWrite();
- } else if (uri.getScheme().equals(UserAdminConf.SCHEME_LDAP)) {
- if (uri.getAuthority() != null)// assume writable if authenticated
- return false;
- } else if (uri.getScheme().equals(UserAdminConf.SCHEME_OS)) {
- return true;
- }
- return true;// read only by default
- }
-
- public boolean isReadOnly() {
- return readOnly;
- }
-
- public boolean isDisabled() {
- return disabled;
- }
-
- protected UserAdmin getExternalRoles() {
- return externalRoles;
- }
-
- protected int roleType(LdapName dn) {
- if (dn.startsWith(groupBaseDn))
- return Role.GROUP;
- else if (dn.startsWith(userBaseDn))
- return Role.USER;
- else
- return Role.GROUP;
- }
-
- /** dn can be null, in that case a default should be returned. */
- public String getUserObjectClass() {
- return userObjectClass;
- }
-
- public String getUserBase() {
- return userBase;
- }
-
- protected String newUserObjectClass(LdapName dn) {
- return getUserObjectClass();
- }
-
- public String getGroupObjectClass() {
- return groupObjectClass;
- }
-
- public String getGroupBase() {
- return groupBase;
- }
-
- public LdapName getBaseDn() {
- return (LdapName) baseDn.clone();
- }
-
- public Dictionary<String, Object> getProperties() {
- return properties;
- }
-
- public Dictionary<String, Object> cloneProperties() {
- return new Hashtable<>(properties);
- }
-
- public void setExternalRoles(UserAdmin externalRoles) {
- this.externalRoles = externalRoles;
- }
-
-// public void setTransactionManager(TransactionManager transactionManager) {
-// this.transactionManager = transactionManager;
-// }
-
- public void setTransactionControl(WorkControl transactionControl) {
- this.transactionControl = transactionControl;
- }
-
- public WcXaResource getXaResource() {
- return xaResource;
- }
-
- public boolean isScoped() {
- return scoped;
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import javax.security.auth.x500.X500Principal;
-
-import org.osgi.service.useradmin.Authorization;
-
-/** An {@link Authorization} which combines roles form various auth sources. */
-class AggregatingAuthorization implements Authorization {
- private final String name;
- private final String displayName;
- private final Set<String> systemRoles;
- private final Set<String> roles;
-
- public AggregatingAuthorization(String name, String displayName, Set<String> systemRoles, String[] roles) {
- this.name = new X500Principal(name).getName();
- this.displayName = displayName;
- this.systemRoles = Collections.unmodifiableSet(new HashSet<>(systemRoles));
- Set<String> temp = new HashSet<>();
- for (String role : roles) {
- if (!temp.contains(role))
- temp.add(role);
- }
- this.roles = Collections.unmodifiableSet(temp);
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- @Override
- public boolean hasRole(String name) {
- if (systemRoles.contains(name))
- return true;
- if (roles.contains(name))
- return true;
- return false;
- }
-
- @Override
- public String[] getRoles() {
- int size = systemRoles.size() + roles.size();
- List<String> res = new ArrayList<String>(size);
- res.addAll(systemRoles);
- res.addAll(roles);
- return res.toArray(new String[size]);
- }
-
- @Override
- public int hashCode() {
- if (name == null)
- return super.hashCode();
- return name.hashCode();
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof Authorization))
- return false;
- Authorization that = (Authorization) obj;
- if (name == null)
- return that.getName() == null;
- return name.equals(that.getName());
- }
-
- @Override
- public String toString() {
- return displayName;
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import javax.naming.InvalidNameException;
-import javax.naming.ldap.LdapName;
-
-import org.osgi.framework.InvalidSyntaxException;
-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.UserAdmin;
-
-/**
- * Aggregates multiple {@link UserDirectory} and integrates them with system
- * roles.
- */
-public class AggregatingUserAdmin implements UserAdmin {
- private final LdapName systemRolesBaseDn;
- private final LdapName tokensBaseDn;
-
- // DAOs
- private AbstractUserDirectory systemRoles = null;
- private AbstractUserDirectory tokens = null;
- private Map<LdapName, AbstractUserDirectory> businessRoles = new HashMap<LdapName, AbstractUserDirectory>();
-
- public AggregatingUserAdmin(String systemRolesBaseDn, String tokensBaseDn) {
- try {
- this.systemRolesBaseDn = new LdapName(systemRolesBaseDn);
- if (tokensBaseDn != null)
- this.tokensBaseDn = new LdapName(tokensBaseDn);
- else
- this.tokensBaseDn = null;
- } catch (InvalidNameException e) {
- throw new UserDirectoryException("Cannot initialize " + AggregatingUserAdmin.class, e);
- }
- }
-
- @Override
- public Role createRole(String name, int type) {
- return findUserAdmin(name).createRole(name, type);
- }
-
- @Override
- public boolean removeRole(String name) {
- boolean actuallyDeleted = findUserAdmin(name).removeRole(name);
- systemRoles.removeRole(name);
- return actuallyDeleted;
- }
-
- @Override
- public Role getRole(String name) {
- return findUserAdmin(name).getRole(name);
- }
-
- @Override
- public Role[] getRoles(String filter) throws InvalidSyntaxException {
- List<Role> res = new ArrayList<Role>();
- for (UserAdmin userAdmin : businessRoles.values()) {
- res.addAll(Arrays.asList(userAdmin.getRoles(filter)));
- }
- res.addAll(Arrays.asList(systemRoles.getRoles(filter)));
- return res.toArray(new Role[res.size()]);
- }
-
- @Override
- public User getUser(String key, String value) {
- List<User> res = new ArrayList<User>();
- for (UserAdmin userAdmin : businessRoles.values()) {
- User u = userAdmin.getUser(key, value);
- if (u != null)
- res.add(u);
- }
- // Note: node roles cannot contain users, so it is not searched
- return res.size() == 1 ? res.get(0) : null;
- }
-
- @Override
- public Authorization getAuthorization(User user) {
- if (user == null) {// anonymous
- return systemRoles.getAuthorization(null);
- }
- AbstractUserDirectory userReferentialOfThisUser = findUserAdmin(user.getName());
- Authorization rawAuthorization = userReferentialOfThisUser.getAuthorization(user);
- String usernameToUse;
- String displayNameToUse;
- if (user instanceof Group) {
- // TODO check whether this is still working
- String ownerDn = TokenUtils.userDn((Group) user);
- if (ownerDn != null) {// tokens
- UserAdmin ownerUserAdmin = findUserAdmin(ownerDn);
- User ownerUser = (User) ownerUserAdmin.getRole(ownerDn);
- usernameToUse = ownerDn;
- displayNameToUse = LdifAuthorization.extractDisplayName(ownerUser);
- } else {
- usernameToUse = rawAuthorization.getName();
- displayNameToUse = rawAuthorization.toString();
- }
- } else {// regular users
- usernameToUse = rawAuthorization.getName();
- displayNameToUse = rawAuthorization.toString();
- }
-
- // gather roles from other referentials
- final AbstractUserDirectory userAdminToUse;// possibly scoped when authenticating
- if (user instanceof DirectoryUser) {
- userAdminToUse = userReferentialOfThisUser;
- } else if (user instanceof AuthenticatingUser) {
- userAdminToUse = userReferentialOfThisUser.scope(user);
- } else {
- throw new IllegalArgumentException("Unsupported user type " + user.getClass());
- }
-
- try {
- Set<String> sysRoles = new HashSet<String>();
- for (String role : rawAuthorization.getRoles()) {
- User userOrGroup = (User) userAdminToUse.getRole(role);
- Authorization auth = systemRoles.getAuthorization(userOrGroup);
- systemRoles: for (String systemRole : auth.getRoles()) {
- if (role.equals(systemRole))
- continue systemRoles;
- sysRoles.add(systemRole);
- }
-// sysRoles.addAll(Arrays.asList(auth.getRoles()));
- }
- addAbstractSystemRoles(rawAuthorization, sysRoles);
- Authorization authorization = new AggregatingAuthorization(usernameToUse, displayNameToUse, sysRoles,
- rawAuthorization.getRoles());
- return authorization;
- } finally {
- if (userAdminToUse != null && userAdminToUse.isScoped()) {
- userAdminToUse.destroy();
- }
- }
- }
-
- /**
- * Enrich with application-specific roles which are strictly programmatic, such
- * as anonymous/user semantics.
- */
- protected void addAbstractSystemRoles(Authorization rawAuthorization, Set<String> sysRoles) {
-
- }
-
- //
- // USER ADMIN AGGREGATOR
- //
- protected void addUserDirectory(AbstractUserDirectory userDirectory) {
- LdapName baseDn = userDirectory.getBaseDn();
- if (isSystemRolesBaseDn(baseDn)) {
- this.systemRoles = userDirectory;
- systemRoles.setExternalRoles(this);
- } else if (isTokensBaseDn(baseDn)) {
- this.tokens = userDirectory;
- tokens.setExternalRoles(this);
- } else {
- if (businessRoles.containsKey(baseDn))
- throw new UserDirectoryException("There is already a user admin for " + baseDn);
- businessRoles.put(baseDn, userDirectory);
- }
- userDirectory.init();
- postAdd(userDirectory);
- }
-
- /** Called after a new user directory has been added */
- protected void postAdd(AbstractUserDirectory userDirectory) {
- }
-
-// private UserAdmin findUserAdmin(User user) {
-// if (user == null)
-// throw new IllegalArgumentException("User should not be null");
-// AbstractUserDirectory userAdmin = findUserAdmin(user.getName());
-// if (user instanceof DirectoryUser) {
-// return userAdmin;
-// } else {
-// return userAdmin.scope(user);
-// }
-// }
-
- private AbstractUserDirectory findUserAdmin(String name) {
- try {
- return findUserAdmin(new LdapName(name));
- } catch (InvalidNameException e) {
- throw new UserDirectoryException("Badly formatted name " + name, e);
- }
- }
-
- private AbstractUserDirectory findUserAdmin(LdapName name) {
- if (name.startsWith(systemRolesBaseDn))
- return systemRoles;
- if (tokensBaseDn != null && name.startsWith(tokensBaseDn))
- return tokens;
- List<AbstractUserDirectory> res = new ArrayList<>(1);
- userDirectories: for (LdapName baseDn : businessRoles.keySet()) {
- AbstractUserDirectory userDirectory = businessRoles.get(baseDn);
- if (name.startsWith(baseDn)) {
- if (userDirectory.isDisabled())
- continue userDirectories;
-// if (res.isEmpty()) {
- res.add(userDirectory);
-// } else {
-// for (AbstractUserDirectory ud : res) {
-// LdapName bd = ud.getBaseDn();
-// if (userDirectory.getBaseDn().startsWith(bd)) {
-// // child user directory
-// }
-// }
-// }
- }
- }
- if (res.size() == 0)
- throw new UserDirectoryException("Cannot find user admin for " + name);
- if (res.size() > 1)
- throw new UserDirectoryException("Multiple user admin found for " + name);
- return res.get(0);
- }
-
- protected boolean isSystemRolesBaseDn(LdapName baseDn) {
- return baseDn.equals(systemRolesBaseDn);
- }
-
- protected boolean isTokensBaseDn(LdapName baseDn) {
- return tokensBaseDn != null && baseDn.equals(tokensBaseDn);
- }
-
-// protected Dictionary<String, Object> currentState() {
-// Dictionary<String, Object> res = new Hashtable<String, Object>();
-// // res.put(NodeConstants.CN, NodeConstants.DEFAULT);
-// for (LdapName name : businessRoles.keySet()) {
-// AbstractUserDirectory userDirectory = businessRoles.get(name);
-// String uri = UserAdminConf.propertiesAsUri(userDirectory.getProperties()).toString();
-// res.put(uri, "");
-// }
-// return res;
-// }
-
- public void destroy() {
- for (LdapName name : businessRoles.keySet()) {
- AbstractUserDirectory userDirectory = businessRoles.get(name);
- destroy(userDirectory);
- }
- businessRoles.clear();
- businessRoles = null;
- destroy(systemRoles);
- systemRoles = null;
- }
-
- private void destroy(AbstractUserDirectory userDirectory) {
- preDestroy(userDirectory);
- userDirectory.destroy();
- }
-
- protected void removeUserDirectory(LdapName baseDn) {
- if (isSystemRolesBaseDn(baseDn))
- throw new UserDirectoryException("System roles cannot be removed ");
- if (!businessRoles.containsKey(baseDn))
- throw new UserDirectoryException("No user directory registered for " + baseDn);
- AbstractUserDirectory userDirectory = businessRoles.remove(baseDn);
- destroy(userDirectory);
- }
-
- /**
- * Called before each user directory is destroyed, so that additional actions
- * can be performed.
- */
- protected void preDestroy(AbstractUserDirectory userDirectory) {
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.util.Dictionary;
-import java.util.Hashtable;
-
-import javax.naming.ldap.LdapName;
-
-import org.osgi.service.useradmin.User;
-
-/**
- * A special user type used during authentication in order to provide the
- * credentials required for scoping the user admin.
- */
-public class AuthenticatingUser implements User {
- /** From com.sun.security.auth.module.*LoginModule */
- public final static String SHARED_STATE_NAME = "javax.security.auth.login.name";
- /** From com.sun.security.auth.module.*LoginModule */
- public final static String SHARED_STATE_PWD = "javax.security.auth.login.password";
-
- private final String name;
- private final Dictionary<String, Object> credentials;
-
- public AuthenticatingUser(LdapName name) {
- if (name == null)
- throw new NullPointerException("Provided name cannot be null.");
- this.name = name.toString();
- this.credentials = new Hashtable<>();
- }
-
- public AuthenticatingUser(String name, Dictionary<String, Object> credentials) {
- this.name = name;
- this.credentials = credentials;
- }
-
- public AuthenticatingUser(String name, char[] password) {
- if (name == null)
- throw new NullPointerException("Provided name cannot be null.");
- this.name = name;
- credentials = new Hashtable<>();
- credentials.put(SHARED_STATE_NAME, name);
- byte[] pwd = DigestUtils.charsToBytes(password);
- credentials.put(SHARED_STATE_PWD, pwd);
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- @Override
- public int getType() {
- return User.USER;
- }
-
- @SuppressWarnings({ "rawtypes", "unchecked" })
- @Override
- public Dictionary getProperties() {
- throw new UnsupportedOperationException();
- }
-
- @SuppressWarnings({ "rawtypes", "unchecked" })
- @Override
- public Dictionary getCredentials() {
- return credentials;
- }
-
- @Override
- public boolean hasCredential(String key, Object value) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public int hashCode() {
- return name.hashCode();
- }
-
- @Override
- public String toString() {
- return "Authenticating user " + name;
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.math.BigInteger;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.spec.KeySpec;
-import java.util.Arrays;
-
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.PBEKeySpec;
-
-/** Utilities around digests, mostly those related to passwords. */
-class DigestUtils {
- final static String PASSWORD_SCHEME_SHA = "SHA";
- final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256";
-
- static byte[] sha1(byte[] bytes) {
- try {
- MessageDigest digest = MessageDigest.getInstance("SHA1");
- digest.update(bytes);
- byte[] checksum = digest.digest();
- return checksum;
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot SHA1 digest", e);
- }
- }
-
- static byte[] toPasswordScheme(String passwordScheme, char[] password, byte[] salt, Integer iterations,
- Integer keyLength) {
- try {
- if (PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
- MessageDigest digest = MessageDigest.getInstance("SHA1");
- byte[] bytes = charsToBytes(password);
- digest.update(bytes);
- return digest.digest();
- } else if (PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) {
- KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength);
-
- SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
- final int ITERATION_LENGTH = 4;
- byte[] key = f.generateSecret(spec).getEncoded();
- byte[] result = new byte[ITERATION_LENGTH + salt.length + key.length];
- byte iterationsArr[] = new BigInteger(iterations.toString()).toByteArray();
- if (iterationsArr.length < ITERATION_LENGTH) {
- Arrays.fill(result, 0, ITERATION_LENGTH - iterationsArr.length, (byte) 0);
- System.arraycopy(iterationsArr, 0, result, ITERATION_LENGTH - iterationsArr.length,
- iterationsArr.length);
- } else {
- System.arraycopy(iterationsArr, 0, result, 0, ITERATION_LENGTH);
- }
- System.arraycopy(salt, 0, result, ITERATION_LENGTH, salt.length);
- System.arraycopy(key, 0, result, ITERATION_LENGTH + salt.length, key.length);
- return result;
- } else {
- throw new UnsupportedOperationException("Unkown password scheme " + passwordScheme);
- }
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot digest", e);
- }
- }
-
- static char[] bytesToChars(Object obj) {
- if (obj instanceof char[])
- return (char[]) obj;
- if (!(obj instanceof byte[]))
- throw new IllegalArgumentException(obj.getClass() + " is not a byte array");
- ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj);
- CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer);
- char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(), toBuffer.limit());
- // Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data
- // Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data
- // Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data
- return res;
- }
-
- static byte[] charsToBytes(char[] chars) {
- CharBuffer charBuffer = CharBuffer.wrap(chars);
- ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
- byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
- // Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data
- // Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
- return bytes;
- }
-
- static String sha1str(String str) {
- byte[] hash = sha1(str.getBytes(StandardCharsets.UTF_8));
- return encodeHexString(hash);
- }
-
- final private static char[] hexArray = "0123456789abcdef".toCharArray();
-
- /**
- * From
- * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
- * -a-hex-string-in-java
- */
- public static String encodeHexString(byte[] bytes) {
- char[] hexChars = new char[bytes.length * 2];
- for (int j = 0; j < bytes.length; j++) {
- int v = bytes[j] & 0xFF;
- hexChars[j * 2] = hexArray[v >>> 4];
- hexChars[j * 2 + 1] = hexArray[v & 0x0F];
- }
- return new String(hexChars);
- }
-
- private DigestUtils() {
- }
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.util.List;
-
-import javax.naming.ldap.LdapName;
-
-import org.osgi.service.useradmin.Group;
-
-/** A group in a user directroy. */
-interface DirectoryGroup extends Group, DirectoryUser {
- List<LdapName> getMemberNames();
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapName;
-
-import org.osgi.service.useradmin.User;
-
-/** A user in a user directory. */
-interface DirectoryUser extends User {
- LdapName getDn();
-
- Attributes getAttributes();
-
- void publishAttributes(Attributes modifiedAttributes);
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.io.IOException;
-import java.net.InetAddress;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Dictionary;
-import java.util.Hashtable;
-import java.util.List;
-
-import javax.naming.InvalidNameException;
-import javax.naming.NamingException;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.naming.DnsBrowser;
-import org.argeo.naming.LdapAttrs;
-
-/** Free IPA specific conventions. */
-public class IpaUtils {
- public final static String IPA_USER_BASE = "cn=users,cn=accounts";
- public final static String IPA_GROUP_BASE = "cn=groups,cn=accounts";
- public final static String IPA_SERVICE_BASE = "cn=services,cn=accounts";
-
- private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase();
-
- public final static String IPA_USER_DIRECTORY_CONFIG = UserAdminConf.userBase + "=" + IPA_USER_BASE + "&"
- + UserAdminConf.groupBase + "=" + IPA_GROUP_BASE + "&" + UserAdminConf.readOnly + "=true";
-
- @Deprecated
- static String domainToUserDirectoryConfigPath(String realm) {
- return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + UserAdminConf.realm.name() + "=" + realm;
- }
-
- public static void addIpaConfig(String realm, Dictionary<String, Object> properties) {
- properties.put(UserAdminConf.baseDn.name(), domainToBaseDn(realm));
- properties.put(UserAdminConf.realm.name(), realm);
- properties.put(UserAdminConf.userBase.name(), IPA_USER_BASE);
- properties.put(UserAdminConf.groupBase.name(), IPA_GROUP_BASE);
- properties.put(UserAdminConf.readOnly.name(), Boolean.TRUE.toString());
- }
-
- public static String domainToBaseDn(String domain) {
- String[] dcs = domain.split("\\.");
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < dcs.length; i++) {
- if (i != 0)
- sb.append(',');
- String dc = dcs[i];
- sb.append(LdapAttrs.dc.name()).append('=').append(dc.toLowerCase());
- }
- return sb.toString();
- }
-
- public static LdapName kerberosToDn(String kerberosName) {
- String[] kname = kerberosName.split("@");
- String username = kname[0];
- String baseDn = domainToBaseDn(kname[1]);
- String dn;
- if (!username.contains("/"))
- dn = LdapAttrs.uid + "=" + username + "," + IPA_USER_BASE + "," + baseDn;
- else
- dn = KRB_PRINCIPAL_NAME + "=" + kerberosName + "," + IPA_SERVICE_BASE + "," + baseDn;
- try {
- return new LdapName(dn);
- } catch (InvalidNameException e) {
- throw new IllegalArgumentException("Badly formatted name for " + kerberosName + ": " + dn);
- }
- }
-
- private IpaUtils() {
-
- }
-
- public static String kerberosDomainFromDns() {
- String kerberosDomain;
- try (DnsBrowser dnsBrowser = new DnsBrowser()) {
- InetAddress localhost = InetAddress.getLocalHost();
- String hostname = localhost.getHostName();
- String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
- kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
- return kerberosDomain;
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot determine Kerberos domain from DNS", e);
- }
-
- }
-
- public static Dictionary<String, Object> convertIpaUri(URI uri) {
- String path = uri.getPath();
- String kerberosRealm;
- if (path == null || path.length() <= 1) {
- kerberosRealm = kerberosDomainFromDns();
- } else {
- kerberosRealm = path.substring(1);
- }
-
- if (kerberosRealm == null)
- throw new UserDirectoryException("No Kerberos domain available for " + uri);
- // TODO intergrate CA certificate in truststore
- // String schemeToUse = SCHEME_LDAPS;
- String schemeToUse = UserAdminConf.SCHEME_LDAP;
- List<String> ldapHosts;
- String ldapHostsStr = uri.getHost();
- if (ldapHostsStr == null || ldapHostsStr.trim().equals("")) {
- try (DnsBrowser dnsBrowser = new DnsBrowser()) {
- ldapHosts = dnsBrowser.getSrvRecordsAsHosts("_ldap._tcp." + kerberosRealm.toLowerCase(),
- schemeToUse.equals(UserAdminConf.SCHEME_LDAP) ? true : false);
- if (ldapHosts == null || ldapHosts.size() == 0) {
- throw new UserDirectoryException("Cannot configure LDAP for IPA " + uri);
- } else {
- ldapHostsStr = ldapHosts.get(0);
- }
- } catch (NamingException | IOException e) {
- throw new UserDirectoryException("cannot convert IPA uri " + uri, e);
- }
- } else {
- ldapHosts = new ArrayList<>();
- ldapHosts.add(ldapHostsStr);
- }
-
- StringBuilder uriStr = new StringBuilder();
- try {
- for (String host : ldapHosts) {
- URI convertedUri = new URI(schemeToUse + "://" + host + "/");
- uriStr.append(convertedUri).append(' ');
- }
- } catch (URISyntaxException e) {
- throw new UserDirectoryException("cannot convert IPA uri " + uri, e);
- }
-
- Hashtable<String, Object> res = new Hashtable<>();
- res.put(UserAdminConf.uri.name(), uriStr.toString());
- addIpaConfig(kerberosRealm, res);
- return res;
- }
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.util.Dictionary;
-import java.util.Hashtable;
-
-import javax.naming.CommunicationException;
-import javax.naming.Context;
-import javax.naming.NameNotFoundException;
-import javax.naming.NamingEnumeration;
-import javax.naming.NamingException;
-import javax.naming.directory.Attributes;
-import javax.naming.directory.DirContext;
-import javax.naming.directory.SearchControls;
-import javax.naming.directory.SearchResult;
-import javax.naming.ldap.InitialLdapContext;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.naming.LdapAttrs;
-
-/** A synchronized wrapper for a single {@link InitialLdapContext}. */
-// TODO implement multiple contexts and connection pooling.
-class LdapConnection {
- private InitialLdapContext initialLdapContext = null;
-
- LdapConnection(String url, Dictionary<String, ?> properties) {
- try {
- Hashtable<String, Object> connEnv = new Hashtable<String, Object>();
- connEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
- connEnv.put(Context.PROVIDER_URL, url);
- connEnv.put("java.naming.ldap.attributes.binary", LdapAttrs.userPassword.name());
- // use pooling in order to avoid connection timeout
-// connEnv.put("com.sun.jndi.ldap.connect.pool", "true");
-// connEnv.put("com.sun.jndi.ldap.connect.pool.timeout", 300000);
-
- initialLdapContext = new InitialLdapContext(connEnv, null);
- // StartTlsResponse tls = (StartTlsResponse) ctx
- // .extendedOperation(new StartTlsRequest());
- // tls.negotiate();
- Object securityAuthentication = properties.get(Context.SECURITY_AUTHENTICATION);
- if (securityAuthentication != null)
- initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, securityAuthentication);
- else
- initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
- Object principal = properties.get(Context.SECURITY_PRINCIPAL);
- if (principal != null) {
- initialLdapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, principal.toString());
- Object creds = properties.get(Context.SECURITY_CREDENTIALS);
- if (creds != null) {
- initialLdapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, creds.toString());
- }
- }
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot connect to LDAP", e);
- }
-
- }
-
- public void init() {
-
- }
-
- public void destroy() {
- try {
- // tls.close();
- initialLdapContext.close();
- initialLdapContext = null;
- } catch (NamingException e) {
- e.printStackTrace();
- }
- }
-
- protected InitialLdapContext getLdapContext() {
- return initialLdapContext;
- }
-
- protected void reconnect() throws NamingException {
- initialLdapContext.reconnect(initialLdapContext.getConnectControls());
- }
-
- public synchronized NamingEnumeration<SearchResult> search(LdapName searchBase, String searchFilter,
- SearchControls searchControls) throws NamingException {
- NamingEnumeration<SearchResult> results;
- try {
- results = getLdapContext().search(searchBase, searchFilter, searchControls);
- } catch (CommunicationException e) {
- reconnect();
- results = getLdapContext().search(searchBase, searchFilter, searchControls);
- }
- return results;
- }
-
- public synchronized Attributes getAttributes(LdapName name) throws NamingException {
- try {
- return getLdapContext().getAttributes(name);
- } catch (CommunicationException e) {
- reconnect();
- return getLdapContext().getAttributes(name);
- }
- }
-
- synchronized void prepareChanges(UserDirectoryWorkingCopy wc) throws NamingException {
- // make sure connection will work
- reconnect();
-
- // delete
- for (LdapName dn : wc.getDeletedUsers().keySet()) {
- if (!entryExists(dn))
- throw new UserDirectoryException("User to delete no found " + dn);
- }
- // add
- for (LdapName dn : wc.getNewUsers().keySet()) {
- if (entryExists(dn))
- throw new UserDirectoryException("User to create found " + dn);
- }
- // modify
- for (LdapName dn : wc.getModifiedUsers().keySet()) {
- if (!wc.getNewUsers().containsKey(dn) && !entryExists(dn))
- throw new UserDirectoryException("User to modify not found " + dn);
- }
-
- }
-
- protected boolean entryExists(LdapName dn) throws NamingException {
- try {
- return getAttributes(dn).size() != 0;
- } catch (NameNotFoundException e) {
- return false;
- }
- }
-
- synchronized void commitChanges(UserDirectoryWorkingCopy wc) throws NamingException {
- // delete
- for (LdapName dn : wc.getDeletedUsers().keySet()) {
- getLdapContext().destroySubcontext(dn);
- }
- // add
- for (LdapName dn : wc.getNewUsers().keySet()) {
- DirectoryUser user = wc.getNewUsers().get(dn);
- getLdapContext().createSubcontext(dn, user.getAttributes());
- }
- // modify
- for (LdapName dn : wc.getModifiedUsers().keySet()) {
- Attributes modifiedAttrs = wc.getModifiedUsers().get(dn);
- getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs);
- }
- }
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import static org.argeo.naming.LdapAttrs.objectClass;
-
-import java.util.ArrayList;
-import java.util.Dictionary;
-import java.util.List;
-
-import javax.naming.AuthenticationNotSupportedException;
-import javax.naming.Binding;
-import javax.naming.Context;
-import javax.naming.InvalidNameException;
-import javax.naming.NameNotFoundException;
-import javax.naming.NamingEnumeration;
-import javax.naming.NamingException;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.naming.directory.SearchControls;
-import javax.naming.directory.SearchResult;
-import javax.naming.ldap.LdapName;
-
-import org.osgi.framework.Filter;
-import org.osgi.service.useradmin.Role;
-import org.osgi.service.useradmin.User;
-
-/** A user admin based on a LDAP server. */
-public class LdapUserAdmin extends AbstractUserDirectory {
- private LdapConnection ldapConnection;
-
- public LdapUserAdmin(Dictionary<String, ?> properties) {
- this(properties, false);
- }
-
- public LdapUserAdmin(Dictionary<String, ?> properties, boolean scoped) {
- super(null, properties, scoped);
- ldapConnection = new LdapConnection(getUri().toString(), properties);
- }
-
- public void destroy() {
- ldapConnection.destroy();
- }
-
- @Override
- protected AbstractUserDirectory scope(User user) {
- Dictionary<String, Object> credentials = user.getCredentials();
- String username = (String) credentials.get(SHARED_STATE_USERNAME);
- if (username == null)
- username = user.getName();
- Dictionary<String, Object> properties = cloneProperties();
- properties.put(Context.SECURITY_PRINCIPAL, username.toString());
- Object pwdCred = credentials.get(SHARED_STATE_PASSWORD);
- byte[] pwd = (byte[]) pwdCred;
- if (pwd != null) {
- char[] password = DigestUtils.bytesToChars(pwd);
- properties.put(Context.SECURITY_CREDENTIALS, new String(password));
- } else {
- properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
- }
- return new LdapUserAdmin(properties, true);
- }
-
-// protected InitialLdapContext getLdapContext() {
-// return initialLdapContext;
-// }
-
- @Override
- protected Boolean daoHasRole(LdapName dn) {
- try {
- return daoGetRole(dn) != null;
- } catch (NameNotFoundException e) {
- return false;
- }
- }
-
- @Override
- protected DirectoryUser daoGetRole(LdapName name) throws NameNotFoundException {
- try {
- Attributes attrs = ldapConnection.getAttributes(name);
- if (attrs.size() == 0)
- return null;
- int roleType = roleType(name);
- LdifUser res;
- if (roleType == Role.GROUP)
- res = new LdifGroup(this, name, attrs);
- else if (roleType == Role.USER)
- res = new LdifUser(this, name, attrs);
- else
- throw new UserDirectoryException("Unsupported LDAP type for " + name);
- return res;
- } catch (NameNotFoundException e) {
- throw e;
- } catch (NamingException e) {
- return null;
- }
- }
-
- @Override
- protected List<DirectoryUser> doGetRoles(Filter f) {
- ArrayList<DirectoryUser> res = new ArrayList<DirectoryUser>();
- try {
- String searchFilter = f != null ? f.toString()
- : "(|(" + objectClass + "=" + getUserObjectClass() + ")(" + objectClass + "="
- + getGroupObjectClass() + "))";
- SearchControls searchControls = new SearchControls();
- searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
-
- LdapName searchBase = getBaseDn();
- NamingEnumeration<SearchResult> results = ldapConnection.search(searchBase, searchFilter, searchControls);
-
- results: while (results.hasMoreElements()) {
- SearchResult searchResult = results.next();
- Attributes attrs = searchResult.getAttributes();
- Attribute objectClassAttr = attrs.get(objectClass.name());
- LdapName dn = toDn(searchBase, searchResult);
- LdifUser role;
- if (objectClassAttr.contains(getGroupObjectClass())
- || objectClassAttr.contains(getGroupObjectClass().toLowerCase()))
- role = new LdifGroup(this, dn, attrs);
- else if (objectClassAttr.contains(getUserObjectClass())
- || objectClassAttr.contains(getUserObjectClass().toLowerCase()))
- role = new LdifUser(this, dn, attrs);
- else {
-// log.warn("Unsupported LDAP type for " + searchResult.getName());
- continue results;
- }
- res.add(role);
- }
- return res;
- } catch (AuthenticationNotSupportedException e) {
- // ignore (typically an unsupported anonymous bind)
- // TODO better logging
- return res;
- } catch (Exception e) {
- e.printStackTrace();
- throw new UserDirectoryException("Cannot get roles for filter " + f, e);
- }
- }
-
- private LdapName toDn(LdapName baseDn, Binding binding) throws InvalidNameException {
- return new LdapName(binding.isRelative() ? binding.getName() + "," + baseDn : binding.getName());
- }
-
- @Override
- protected List<LdapName> getDirectGroups(LdapName dn) {
- List<LdapName> directGroups = new ArrayList<LdapName>();
- try {
- String searchFilter = "(&(" + objectClass + "=" + getGroupObjectClass() + ")(" + getMemberAttributeId()
- + "=" + dn + "))";
-
- SearchControls searchControls = new SearchControls();
- searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
-
- LdapName searchBase = getBaseDn();
- NamingEnumeration<SearchResult> results = ldapConnection.search(searchBase, searchFilter, searchControls);
-
- while (results.hasMoreElements()) {
- SearchResult searchResult = (SearchResult) results.nextElement();
- directGroups.add(toDn(searchBase, searchResult));
- }
- return directGroups;
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot populate direct members of " + dn, e);
- }
- }
-
- @Override
- protected void prepare(UserDirectoryWorkingCopy wc) {
- try {
- ldapConnection.prepareChanges(wc);
- } catch (NamingException e) {
- throw new UserDirectoryException("Cannot prepare LDAP", e);
- }
- }
-
- @Override
- protected void commit(UserDirectoryWorkingCopy wc) {
- try {
- ldapConnection.commitChanges(wc);
- } catch (NamingException e) {
- throw new UserDirectoryException("Cannot commit LDAP", e);
- }
- }
-
- @Override
- protected void rollback(UserDirectoryWorkingCopy wc) {
- // prepare not impacting
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Dictionary;
-import java.util.List;
-
-import org.argeo.naming.LdapAttrs;
-import org.osgi.service.useradmin.Authorization;
-import org.osgi.service.useradmin.Role;
-import org.osgi.service.useradmin.User;
-
-/** Basic authorization. */
-class LdifAuthorization implements Authorization {
- private final String name;
- private final String displayName;
- private final List<String> allRoles;
-
- public LdifAuthorization(User user, List<Role> allRoles) {
- if (user == null) {
- this.name = null;
- this.displayName = "anonymous";
- } else {
- this.name = user.getName();
- this.displayName = extractDisplayName(user);
- }
- // roles
- String[] roles = new String[allRoles.size()];
- for (int i = 0; i < allRoles.size(); i++) {
- roles[i] = allRoles.get(i).getName();
- }
- this.allRoles = Collections.unmodifiableList(Arrays.asList(roles));
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- @Override
- public boolean hasRole(String name) {
- return allRoles.contains(name);
- }
-
- @Override
- public String[] getRoles() {
- return allRoles.toArray(new String[allRoles.size()]);
- }
-
- @Override
- public int hashCode() {
- if (name == null)
- return super.hashCode();
- return name.hashCode();
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof Authorization))
- return false;
- Authorization that = (Authorization) obj;
- if (name == null)
- return that.getName() == null;
- return name.equals(that.getName());
- }
-
- @Override
- public String toString() {
- return displayName;
- }
-
- final static String extractDisplayName(User user) {
- Dictionary<String, Object> props = user.getProperties();
- Object displayName = props.get(LdapAttrs.displayName);
- if (displayName == null)
- displayName = props.get(LdapAttrs.cn);
- if (displayName == null)
- displayName = props.get(LdapAttrs.uid);
- if (displayName == null)
- displayName = user.getName();
- if (displayName == null)
- throw new UserDirectoryException("Cannot set display name for " + user);
- return displayName.toString();
- }
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.naming.InvalidNameException;
-import javax.naming.NamingEnumeration;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapName;
-
-import org.osgi.service.useradmin.Role;
-
-/** Directory group implementation */
-class LdifGroup extends LdifUser implements DirectoryGroup {
- private final String memberAttributeId;
-
- LdifGroup(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) {
- super(userAdmin, dn, attributes);
- memberAttributeId = userAdmin.getMemberAttributeId();
- }
-
- @Override
- public boolean addMember(Role role) {
- try {
- Role foundRole = findRole(new LdapName(role.getName()));
- if (foundRole == null)
- throw new UnsupportedOperationException(
- "Adding role " + role.getName() + " is unsupported within this context.");
- } catch (InvalidNameException e) {
- throw new IllegalArgumentException("Role name" + role.getName() + " is badly formatted");
- }
-
- getUserAdmin().checkEdit();
- if (!isEditing())
- startEditing();
-
- Attribute member = getAttributes().get(memberAttributeId);
- if (member != null) {
- if (member.contains(role.getName()))
- return false;
- else
- member.add(role.getName());
- } else
- getAttributes().put(memberAttributeId, role.getName());
- return true;
- }
-
- @Override
- public boolean addRequiredMember(Role role) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean removeMember(Role role) {
- getUserAdmin().checkEdit();
- if (!isEditing())
- startEditing();
-
- Attribute member = getAttributes().get(memberAttributeId);
- if (member != null) {
- if (!member.contains(role.getName()))
- return false;
- member.remove(role.getName());
- return true;
- } else
- return false;
- }
-
- @Override
- public Role[] getMembers() {
- List<Role> directMembers = new ArrayList<Role>();
- for (LdapName ldapName : getMemberNames()) {
- Role role = findRole(ldapName);
- if (role == null) {
- throw new UserDirectoryException("Role " + ldapName + " cannot be added.");
- }
- directMembers.add(role);
- }
- return directMembers.toArray(new Role[directMembers.size()]);
- }
-
- /**
- * Whether a role with this name can be found from this context.
- *
- * @return The related {@link Role} or <code>null</code>.
- */
- protected Role findRole(LdapName ldapName) {
- Role role = getUserAdmin().getRole(ldapName.toString());
- if (role == null) {
- if (getUserAdmin().getExternalRoles() != null)
- role = getUserAdmin().getExternalRoles().getRole(ldapName.toString());
- }
- return role;
- }
-
- @Override
- public List<LdapName> getMemberNames() {
- Attribute memberAttribute = getAttributes().get(memberAttributeId);
- if (memberAttribute == null)
- return new ArrayList<LdapName>();
- try {
- List<LdapName> roles = new ArrayList<LdapName>();
- NamingEnumeration<?> values = memberAttribute.getAll();
- while (values.hasMore()) {
- LdapName dn = new LdapName(values.next().toString());
- roles.add(dn);
- }
- return roles;
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot get members", e);
- }
- }
-
- @Override
- public Role[] getRequiredMembers() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public int getType() {
- return GROUP;
- }
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import static java.nio.charset.StandardCharsets.US_ASCII;
-
-import java.math.BigInteger;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Base64;
-import java.util.Collections;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
-
-import javax.naming.NamingEnumeration;
-import javax.naming.NamingException;
-import javax.naming.directory.Attribute;
-import javax.naming.directory.Attributes;
-import javax.naming.directory.BasicAttribute;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.naming.AuthPassword;
-import org.argeo.naming.LdapAttrs;
-import org.argeo.naming.SharedSecret;
-
-/** Directory user implementation */
-class LdifUser implements DirectoryUser {
- private final AbstractUserDirectory userAdmin;
-
- private final LdapName dn;
-
- private final boolean frozen;
- private Attributes publishedAttributes;
-
- private final AttributeDictionary properties;
- private final AttributeDictionary credentials;
-
- LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) {
- this(userAdmin, dn, attributes, false);
- }
-
- private LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes, boolean frozen) {
- this.userAdmin = userAdmin;
- this.dn = dn;
- this.publishedAttributes = attributes;
- properties = new AttributeDictionary(false);
- credentials = new AttributeDictionary(true);
- this.frozen = frozen;
- }
-
- @Override
- public String getName() {
- return dn.toString();
- }
-
- @Override
- public int getType() {
- return USER;
- }
-
- @Override
- public Dictionary<String, Object> getProperties() {
- return properties;
- }
-
- @Override
- public Dictionary<String, Object> getCredentials() {
- return credentials;
- }
-
- @Override
- public boolean hasCredential(String key, Object value) {
- if (key == null) {
- // TODO check other sources (like PKCS12)
- // String pwd = new String((char[]) value);
- // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112)
- char[] password = DigestUtils.bytesToChars(value);
- AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password);
- if (authPassword != null) {
- if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) {
- SharedSecret onceToken = new SharedSecret(authPassword);
- if (onceToken.isExpired()) {
- // AuthPassword.remove(getAttributes(), onceToken);
- return false;
- } else {
- // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
- return true;
- }
- // TODO delete expired tokens?
- } else {
- // TODO implement SHA
- throw new UnsupportedOperationException(
- "Unsupported authPassword scheme " + authPassword.getAuthScheme());
- }
- }
-
- // Regular password
-// byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256);
- if (hasCredential(LdapAttrs.userPassword.name(), DigestUtils.charsToBytes(password)))
- return true;
- return false;
- }
-
- Object storedValue = getCredentials().get(key);
- if (storedValue == null || value == null)
- return false;
- if (!(value instanceof String || value instanceof byte[]))
- return false;
- if (storedValue instanceof String && value instanceof String)
- return storedValue.equals(value);
- if (storedValue instanceof byte[] && value instanceof byte[]) {
- String storedBase64 = new String((byte[]) storedValue, US_ASCII);
- String passwordScheme = null;
- if (storedBase64.charAt(0) == '{') {
- int index = storedBase64.indexOf('}');
- if (index > 0) {
- passwordScheme = storedBase64.substring(1, index);
- String storedValueBase64 = storedBase64.substring(index + 1);
- byte[] storedValueBytes = Base64.getDecoder().decode(storedValueBase64);
- char[] passwordValue = DigestUtils.bytesToChars((byte[]) value);
- byte[] valueBytes;
- if (DigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
- valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, null);
- } else if (DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) {
- // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/
- byte[] iterationsArr = Arrays.copyOfRange(storedValueBytes, 0, 4);
- BigInteger iterations = new BigInteger(iterationsArr);
- byte[] salt = Arrays.copyOfRange(storedValueBytes, iterationsArr.length,
- iterationsArr.length + 64);
- byte[] keyArr = Arrays.copyOfRange(storedValueBytes, iterationsArr.length + salt.length,
- storedValueBytes.length);
- int keyLengthBits = keyArr.length * 8;
- valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt,
- iterations.intValue(), keyLengthBits);
- } else {
- throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme);
- }
- return Arrays.equals(storedValueBytes, valueBytes);
- }
- }
- }
-// if (storedValue instanceof byte[] && value instanceof byte[]) {
-// return Arrays.equals((byte[]) storedValue, (byte[]) value);
-// }
- return false;
- }
-
- /** Hash the password */
- byte[] sha1hash(char[] password) {
- byte[] hashedPassword = ("{SHA}"
- + Base64.getEncoder().encodeToString(DigestUtils.sha1(DigestUtils.charsToBytes(password))))
- .getBytes(StandardCharsets.UTF_8);
- return hashedPassword;
- }
-
-// byte[] hash(char[] password, String passwordScheme) {
-// if (passwordScheme == null)
-// passwordScheme = DigestUtils.PASSWORD_SCHEME_SHA;
-// byte[] hashedPassword = ("{" + passwordScheme + "}"
-// + Base64.getEncoder().encodeToString(DigestUtils.toPasswordScheme(passwordScheme, password)))
-// .getBytes(US_ASCII);
-// return hashedPassword;
-// }
-
- @Override
- public LdapName getDn() {
- return dn;
- }
-
- @Override
- public synchronized Attributes getAttributes() {
- return isEditing() ? getModifiedAttributes() : publishedAttributes;
- }
-
- /** Should only be called from working copy thread. */
- private synchronized Attributes getModifiedAttributes() {
- assert getWc() != null;
- return getWc().getAttributes(getDn());
- }
-
- protected synchronized boolean isEditing() {
- return getWc() != null && getModifiedAttributes() != null;
- }
-
- private synchronized UserDirectoryWorkingCopy getWc() {
- return userAdmin.getWorkingCopy();
- }
-
- protected synchronized void startEditing() {
- if (frozen)
- throw new UserDirectoryException("Cannot edit frozen view");
- if (getUserAdmin().isReadOnly())
- throw new UserDirectoryException("User directory is read-only");
- assert getModifiedAttributes() == null;
- getWc().startEditing(this);
- // modifiedAttributes = (Attributes) publishedAttributes.clone();
- }
-
- public synchronized void publishAttributes(Attributes modifiedAttributes) {
- publishedAttributes = modifiedAttributes;
- }
-
- public DirectoryUser getPublished() {
- return new LdifUser(userAdmin, dn, publishedAttributes, true);
- }
-
- @Override
- public int hashCode() {
- return dn.hashCode();
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (obj instanceof LdifUser) {
- LdifUser that = (LdifUser) obj;
- return this.dn.equals(that.dn);
- }
- return false;
- }
-
- @Override
- public String toString() {
- return dn.toString();
- }
-
- protected AbstractUserDirectory getUserAdmin() {
- return userAdmin;
- }
-
- private class AttributeDictionary extends Dictionary<String, Object> {
- private final List<String> effectiveKeys = new ArrayList<String>();
- private final List<String> attrFilter;
- private final Boolean includeFilter;
-
- public AttributeDictionary(Boolean includeFilter) {
- this.attrFilter = userAdmin.getCredentialAttributeIds();
- this.includeFilter = includeFilter;
- try {
- NamingEnumeration<String> ids = getAttributes().getIDs();
- while (ids.hasMore()) {
- String id = ids.next();
- if (includeFilter && attrFilter.contains(id))
- effectiveKeys.add(id);
- else if (!includeFilter && !attrFilter.contains(id))
- effectiveKeys.add(id);
- }
- } catch (NamingException e) {
- throw new UserDirectoryException("Cannot initialise attribute dictionary", e);
- }
- }
-
- @Override
- public int size() {
- return effectiveKeys.size();
- }
-
- @Override
- public boolean isEmpty() {
- return effectiveKeys.size() == 0;
- }
-
- @Override
- public Enumeration<String> keys() {
- return Collections.enumeration(effectiveKeys);
- }
-
- @Override
- public Enumeration<Object> elements() {
- final Iterator<String> it = effectiveKeys.iterator();
- return new Enumeration<Object>() {
-
- @Override
- public boolean hasMoreElements() {
- return it.hasNext();
- }
-
- @Override
- public Object nextElement() {
- String key = it.next();
- return get(key);
- }
-
- };
- }
-
- @Override
- public Object get(Object key) {
- try {
- Attribute attr = getAttributes().get(key.toString());
- if (attr == null)
- return null;
- Object value = attr.get();
- if (value instanceof byte[]) {
- if (key.equals(LdapAttrs.userPassword.name()))
- // TODO other cases (certificates, images)
- return value;
- value = new String((byte[]) value, StandardCharsets.UTF_8);
- }
- if (attr.size() == 1)
- return value;
- if (!attr.getID().equals(LdapAttrs.objectClass.name()))
- return value;
- // special case for object class
- NamingEnumeration<?> en = attr.getAll();
- Set<String> objectClasses = new HashSet<String>();
- while (en.hasMore()) {
- String objectClass = en.next().toString();
- objectClasses.add(objectClass);
- }
-
- if (objectClasses.contains(userAdmin.getUserObjectClass()))
- return userAdmin.getUserObjectClass();
- else if (objectClasses.contains(userAdmin.getGroupObjectClass()))
- return userAdmin.getGroupObjectClass();
- else
- return value;
- } catch (NamingException e) {
- throw new UserDirectoryException("Cannot get value for attribute " + key, e);
- }
- }
-
- @Override
- public Object put(String key, Object value) {
- if (key == null) {
- // TODO persist to other sources (like PKCS12)
- char[] password = DigestUtils.bytesToChars(value);
- byte[] hashedPassword = sha1hash(password);
- return put(LdapAttrs.userPassword.name(), hashedPassword);
- }
- if (key.startsWith("X-")) {
- return put(LdapAttrs.authPassword.name(), value);
- }
-
- userAdmin.checkEdit();
- if (!isEditing())
- startEditing();
-
- if (!(value instanceof String || value instanceof byte[]))
- throw new IllegalArgumentException("Value must be String or byte[]");
-
- if (includeFilter && !attrFilter.contains(key))
- throw new IllegalArgumentException("Key " + key + " not included");
- else if (!includeFilter && attrFilter.contains(key))
- throw new IllegalArgumentException("Key " + key + " excluded");
-
- try {
- Attribute attribute = getModifiedAttributes().get(key.toString());
- // if (attribute == null) // block unit tests
- attribute = new BasicAttribute(key.toString());
- if (value instanceof String && !isAsciiPrintable(((String) value)))
- attribute.add(((String) value).getBytes(StandardCharsets.UTF_8));
- else
- attribute.add(value);
- Attribute previousAttribute = getModifiedAttributes().put(attribute);
- if (previousAttribute != null)
- return previousAttribute.get();
- else
- return null;
- } catch (NamingException e) {
- throw new UserDirectoryException("Cannot get value for attribute " + key, e);
- }
- }
-
- @Override
- public Object remove(Object key) {
- userAdmin.checkEdit();
- if (!isEditing())
- startEditing();
-
- if (includeFilter && !attrFilter.contains(key))
- throw new IllegalArgumentException("Key " + key + " not included");
- else if (!includeFilter && attrFilter.contains(key))
- throw new IllegalArgumentException("Key " + key + " excluded");
-
- try {
- Attribute attr = getModifiedAttributes().remove(key.toString());
- if (attr != null)
- return attr.get();
- else
- return null;
- } catch (NamingException e) {
- throw new UserDirectoryException("Cannot remove attribute " + key, e);
- }
- }
- }
-
- private static boolean isAsciiPrintable(String str) {
- if (str == null) {
- return false;
- }
- int sz = str.length();
- for (int i = 0; i < sz; i++) {
- if (isAsciiPrintable(str.charAt(i)) == false) {
- return false;
- }
- }
- return true;
- }
-
- private static boolean isAsciiPrintable(char ch) {
- return ch >= 32 && ch < 127;
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import static org.argeo.naming.LdapAttrs.objectClass;
-import static org.argeo.naming.LdapObjs.inetOrgPerson;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Dictionary;
-import java.util.HashSet;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-import javax.naming.NameNotFoundException;
-import javax.naming.NamingEnumeration;
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.naming.LdifParser;
-import org.argeo.naming.LdifWriter;
-import org.osgi.framework.Filter;
-import org.osgi.service.useradmin.Role;
-import org.osgi.service.useradmin.User;
-
-/** A user admin based on a LDIF files. */
-public class LdifUserAdmin extends AbstractUserDirectory {
- private SortedMap<LdapName, DirectoryUser> users = new TreeMap<LdapName, DirectoryUser>();
- private SortedMap<LdapName, DirectoryGroup> groups = new TreeMap<LdapName, DirectoryGroup>();
-
- public LdifUserAdmin(String uri, String baseDn) {
- this(fromUri(uri, baseDn), false);
- }
-
- public LdifUserAdmin(Dictionary<String, ?> properties) {
- this(properties, false);
- }
-
- protected LdifUserAdmin(Dictionary<String, ?> properties, boolean scoped) {
- super(null, properties, scoped);
- }
-
- public LdifUserAdmin(URI uri, Dictionary<String, ?> properties) {
- super(uri, properties, false);
- }
-
- @Override
- protected AbstractUserDirectory scope(User user) {
- Dictionary<String, Object> credentials = user.getCredentials();
- String username = (String) credentials.get(SHARED_STATE_USERNAME);
- if (username == null)
- username = user.getName();
- Object pwdCred = credentials.get(SHARED_STATE_PASSWORD);
- byte[] pwd = (byte[]) pwdCred;
- if (pwd != null) {
- char[] password = DigestUtils.bytesToChars(pwd);
- User directoryUser = (User) getRole(username);
- if (!directoryUser.hasCredential(null, password))
- throw new UserDirectoryException("Invalid credentials");
- } else {
- throw new UserDirectoryException("Password is required");
- }
- Dictionary<String, Object> properties = cloneProperties();
- properties.put(UserAdminConf.readOnly.name(), "true");
- LdifUserAdmin scopedUserAdmin = new LdifUserAdmin(properties, true);
- scopedUserAdmin.groups = Collections.unmodifiableSortedMap(groups);
- scopedUserAdmin.users = Collections.unmodifiableSortedMap(users);
- return scopedUserAdmin;
- }
-
- private static Dictionary<String, Object> fromUri(String uri, String baseDn) {
- Hashtable<String, Object> res = new Hashtable<String, Object>();
- res.put(UserAdminConf.uri.name(), uri);
- res.put(UserAdminConf.baseDn.name(), baseDn);
- return res;
- }
-
- public void init() {
-
- try {
- URI u = new URI(getUri());
- if (u.getScheme().equals("file")) {
- File file = new File(u);
- if (!file.exists())
- return;
- }
- load(u.toURL().openStream());
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot open URL " + getUri(), e);
- }
- }
-
- public void save() {
- if (getUri() == null)
- throw new UserDirectoryException("Cannot save LDIF user admin: no URI is set");
- if (isReadOnly())
- throw new UserDirectoryException("Cannot save LDIF user admin: " + getUri() + " is read-only");
- try (FileOutputStream out = new FileOutputStream(new File(new URI(getUri())))) {
- save(out);
- } catch (IOException | URISyntaxException e) {
- throw new UserDirectoryException("Cannot save user admin to " + getUri(), e);
- }
- }
-
- public void save(OutputStream out) throws IOException {
- try {
- LdifWriter ldifWriter = new LdifWriter(out);
- for (LdapName name : groups.keySet())
- ldifWriter.writeEntry(name, groups.get(name).getAttributes());
- for (LdapName name : users.keySet())
- ldifWriter.writeEntry(name, users.get(name).getAttributes());
- } finally {
- out.close();
- }
- }
-
- protected void load(InputStream in) {
- try {
- users.clear();
- groups.clear();
-
- LdifParser ldifParser = new LdifParser();
- SortedMap<LdapName, Attributes> allEntries = ldifParser.read(in);
- for (LdapName key : allEntries.keySet()) {
- Attributes attributes = allEntries.get(key);
- // check for inconsistency
- Set<String> lowerCase = new HashSet<String>();
- NamingEnumeration<String> ids = attributes.getIDs();
- while (ids.hasMoreElements()) {
- String id = ids.nextElement().toLowerCase();
- if (lowerCase.contains(id))
- throw new UserDirectoryException(key + " has duplicate id " + id);
- lowerCase.add(id);
- }
-
- // analyse object classes
- NamingEnumeration<?> objectClasses = attributes.get(objectClass.name()).getAll();
- // System.out.println(key);
- objectClasses: while (objectClasses.hasMore()) {
- String objectClass = objectClasses.next().toString();
- // System.out.println(" " + objectClass);
- if (objectClass.toLowerCase().equals(inetOrgPerson.name().toLowerCase())) {
- users.put(key, new LdifUser(this, key, attributes));
- break objectClasses;
- } else if (objectClass.toLowerCase().equals(getGroupObjectClass().toLowerCase())) {
- groups.put(key, new LdifGroup(this, key, attributes));
- break objectClasses;
- }
- }
- }
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot load user admin service from LDIF", e);
- }
- }
-
- public void destroy() {
- if (users == null || groups == null)
- throw new UserDirectoryException("User directory " + getBaseDn() + " is already destroyed");
- users = null;
- groups = null;
- }
-
- @Override
- protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException {
- if (groups.containsKey(key))
- return groups.get(key);
- if (users.containsKey(key))
- return users.get(key);
- throw new NameNotFoundException(key + " not persisted");
- }
-
- @Override
- protected Boolean daoHasRole(LdapName dn) {
- return users.containsKey(dn) || groups.containsKey(dn);
- }
-
- protected List<DirectoryUser> doGetRoles(Filter f) {
- ArrayList<DirectoryUser> res = new ArrayList<DirectoryUser>();
- if (f == null) {
- res.addAll(users.values());
- res.addAll(groups.values());
- } else {
- for (DirectoryUser user : users.values()) {
- if (f.match(user.getProperties()))
- res.add(user);
- }
- for (DirectoryUser group : groups.values())
- if (f.match(group.getProperties()))
- res.add(group);
- }
- return res;
- }
-
- @Override
- protected List<LdapName> getDirectGroups(LdapName dn) {
- List<LdapName> directGroups = new ArrayList<LdapName>();
- for (LdapName name : groups.keySet()) {
- DirectoryGroup group = groups.get(name);
- if (group.getMemberNames().contains(dn))
- directGroups.add(group.getDn());
- }
- return directGroups;
- }
-
- @Override
- protected void prepare(UserDirectoryWorkingCopy wc) {
- // delete
- for (LdapName dn : wc.getDeletedUsers().keySet()) {
- if (users.containsKey(dn))
- users.remove(dn);
- else if (groups.containsKey(dn))
- groups.remove(dn);
- else
- throw new UserDirectoryException("User to delete not found " + dn);
- }
- // add
- for (LdapName dn : wc.getNewUsers().keySet()) {
- DirectoryUser user = wc.getNewUsers().get(dn);
- if (users.containsKey(dn) || groups.containsKey(dn))
- throw new UserDirectoryException("User to create found " + dn);
- else if (Role.USER == user.getType())
- users.put(dn, user);
- else if (Role.GROUP == user.getType())
- groups.put(dn, (DirectoryGroup) user);
- else
- throw new UserDirectoryException("Unsupported role type " + user.getType() + " for new user " + dn);
- }
- // modify
- for (LdapName dn : wc.getModifiedUsers().keySet()) {
- Attributes modifiedAttrs = wc.getModifiedUsers().get(dn);
- DirectoryUser user;
- if (users.containsKey(dn))
- user = users.get(dn);
- else if (groups.containsKey(dn))
- user = groups.get(dn);
- else
- throw new UserDirectoryException("User to modify no found " + dn);
- user.publishAttributes(modifiedAttrs);
- }
- }
-
- @Override
- protected void commit(UserDirectoryWorkingCopy wc) {
- save();
- }
-
- @Override
- protected void rollback(UserDirectoryWorkingCopy wc) {
- init();
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.Dictionary;
-import java.util.List;
-
-import javax.naming.NameNotFoundException;
-import javax.naming.NamingException;
-import javax.naming.directory.Attributes;
-import javax.naming.directory.BasicAttributes;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.naming.LdapAttrs;
-import org.osgi.framework.Filter;
-import org.osgi.service.useradmin.User;
-
-public class OsUserDirectory extends AbstractUserDirectory {
- private final String osUsername = System.getProperty("user.name");
- private final LdapName osUserDn;
- private final LdifUser osUser;
-
- public OsUserDirectory(URI uriArg, Dictionary<String, ?> props) {
- super(uriArg, props, false);
- try {
- osUserDn = new LdapName(LdapAttrs.uid.name() + "=" + osUsername + "," + getUserBase() + "," + getBaseDn());
- Attributes attributes = new BasicAttributes();
- attributes.put(LdapAttrs.uid.name(), osUsername);
- osUser = new LdifUser(this, osUserDn, attributes);
- } catch (NamingException e) {
- throw new UserDirectoryException("Cannot create system user", e);
- }
- }
-
- @Override
- protected List<LdapName> getDirectGroups(LdapName dn) {
- return new ArrayList<>();
- }
-
- @Override
- protected Boolean daoHasRole(LdapName dn) {
- return osUserDn.equals(dn);
- }
-
- @Override
- protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException {
- if (osUserDn.equals(key))
- return osUser;
- else
- throw new NameNotFoundException("Not an OS role");
- }
-
- @Override
- protected List<DirectoryUser> doGetRoles(Filter f) {
- List<DirectoryUser> res = new ArrayList<>();
- if (f == null || f.match(osUser.getProperties()))
- res.add(osUser);
- return res;
- }
-
- @Override
- protected AbstractUserDirectory scope(User user) {
- throw new UnsupportedOperationException();
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.security.NoSuchAlgorithmException;
-import java.security.URIParameter;
-
-import javax.security.auth.Subject;
-import javax.security.auth.login.Configuration;
-import javax.security.auth.login.LoginContext;
-import javax.security.auth.login.LoginException;
-
-public class OsUserUtils {
- private static String LOGIN_CONTEXT_USER_NIX = "USER_NIX";
- private static String LOGIN_CONTEXT_USER_NT = "USER_NT";
-
- public static String getOsUsername() {
- return System.getProperty("user.name");
- }
-
- public static LoginContext loginAsSystemUser(Subject subject) {
- try {
- URL jaasConfigurationUrl = OsUserUtils.class.getClassLoader()
- .getResource("org/argeo/osgi/useradmin/jaas-os.cfg");
- URIParameter uriParameter = new URIParameter(jaasConfigurationUrl.toURI());
- Configuration jaasConfiguration = Configuration.getInstance("JavaLoginConfig", uriParameter);
- LoginContext lc = new LoginContext(isWindows() ? LOGIN_CONTEXT_USER_NT : LOGIN_CONTEXT_USER_NIX, subject,
- null, jaasConfiguration);
- lc.login();
- return lc;
- } catch (URISyntaxException | NoSuchAlgorithmException | LoginException e) {
- throw new RuntimeException("Cannot login as system user", e);
- }
- }
-
- public static void main(String args[]) {
- Subject subject = new Subject();
- LoginContext loginContext = loginAsSystemUser(subject);
- System.out.println(subject);
- try {
- loginContext.logout();
- } catch (LoginException e) {
- // silent
- }
- }
-
- private static boolean isWindows() {
- return System.getProperty("os.name").startsWith("Windows");
- }
-
- private OsUserUtils() {
- }
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import static org.argeo.naming.LdapAttrs.description;
-import static org.argeo.naming.LdapAttrs.owner;
-
-import java.security.Principal;
-import java.time.Instant;
-import java.util.HashSet;
-import java.util.Set;
-
-import javax.naming.InvalidNameException;
-import javax.naming.ldap.LdapName;
-import javax.security.auth.Subject;
-
-import org.argeo.naming.NamingUtils;
-import org.osgi.service.useradmin.Group;
-
-/**
- * Canonically implements the Argeo token conventions.
- */
-public class TokenUtils {
- public static Set<String> tokensUsed(Subject subject, String tokensBaseDn) {
- Set<String> res = new HashSet<>();
- for (Principal principal : subject.getPrincipals()) {
- String name = principal.getName();
- if (name.endsWith(tokensBaseDn)) {
- try {
- LdapName ldapName = new LdapName(name);
- String token = ldapName.getRdn(ldapName.size()).getValue().toString();
- res.add(token);
- } catch (InvalidNameException e) {
- throw new UserDirectoryException("Invalid principal " + principal, e);
- }
- }
- }
- return res;
- }
-
- /** The user related to this token group */
- public static String userDn(Group tokenGroup) {
- return (String) tokenGroup.getProperties().get(owner.name());
- }
-
- public static boolean isExpired(Group tokenGroup) {
- return isExpired(tokenGroup, Instant.now());
-
- }
-
- public static boolean isExpired(Group tokenGroup, Instant instant) {
- String expiryDateStr = (String) tokenGroup.getProperties().get(description.name());
- if (expiryDateStr != null) {
- Instant expiryDate = NamingUtils.ldapDateToInstant(expiryDateStr);
- if (expiryDate.isBefore(instant)) {
- return true;
- }
- }
- return false;
- }
-
-// private final String token;
-//
-// public TokenUtils(String token) {
-// this.token = token;
-// }
-//
-// public String getToken() {
-// return token;
-// }
-//
-// @Override
-// public int hashCode() {
-// return token.hashCode();
-// }
-//
-// @Override
-// public boolean equals(Object obj) {
-// if ((obj instanceof TokenUtils) && ((TokenUtils) obj).token.equals(token))
-// return true;
-// return false;
-// }
-//
-// @Override
-// public String toString() {
-// return "Token #" + hashCode();
-// }
-
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.net.InetAddress;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.UnknownHostException;
-import java.util.Dictionary;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-
-import javax.naming.Context;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.naming.NamingUtils;
-
-/** Properties used to configure user admins. */
-public enum UserAdminConf {
- /** Base DN (cannot be configured externally) */
- baseDn("dc=example,dc=com"),
-
- /** URI of the underlying resource (cannot be configured externally) */
- uri("ldap://localhost:10389"),
-
- /** User objectClass */
- userObjectClass("inetOrgPerson"),
-
- /** Relative base DN for users */
- userBase("ou=People"),
-
- /** Groups objectClass */
- groupObjectClass("groupOfNames"),
-
- /** Relative base DN for users */
- groupBase("ou=Groups"),
-
- /** Read-only source */
- readOnly(null),
-
- /** Disabled source */
- disabled(null),
-
- /** Authentication realm */
- realm(null);
-
- public final static String FACTORY_PID = "org.argeo.osgi.useradmin.config";
-
- public final static String SCHEME_LDAP = "ldap";
- public final static String SCHEME_LDAPS = "ldaps";
- public final static String SCHEME_FILE = "file";
- public final static String SCHEME_OS = "os";
- public final static String SCHEME_IPA = "ipa";
-
- /** The default value. */
- private Object def;
-
- UserAdminConf(Object def) {
- this.def = def;
- }
-
- public Object getDefault() {
- return def;
- }
-
- /**
- * For use as Java property.
- *
- * @deprecated use {@link #name()} instead
- */
- @Deprecated
- public String property() {
- return name();
- }
-
- public String getValue(Dictionary<String, ?> properties) {
- Object res = getRawValue(properties);
- if (res == null)
- return null;
- return res.toString();
- }
-
- @SuppressWarnings("unchecked")
- public <T> T getRawValue(Dictionary<String, ?> properties) {
- Object res = properties.get(name());
- if (res == null)
- res = getDefault();
- return (T) res;
- }
-
- /** @deprecated use {@link #valueOf(String)} instead */
- @Deprecated
- public static UserAdminConf local(String property) {
- return UserAdminConf.valueOf(property);
- }
-
- /** Hides host and credentials. */
- public static URI propertiesAsUri(Dictionary<String, ?> properties) {
- StringBuilder query = new StringBuilder();
-
- boolean first = true;
-// for (Enumeration<String> keys = properties.keys(); keys.hasMoreElements();) {
-// String key = keys.nextElement();
-// // TODO clarify which keys are relevant (list only the enum?)
-// if (!key.equals("service.factoryPid") && !key.equals("cn") && !key.equals("dn")
-// && !key.equals(Constants.SERVICE_PID) && !key.startsWith("java") && !key.equals(baseDn.name())
-// && !key.equals(uri.name()) && !key.equals(Constants.OBJECTCLASS)
-// && !key.equals(Constants.SERVICE_ID) && !key.equals("bundle.id")) {
-// if (first)
-// first = false;
-// else
-// query.append('&');
-// query.append(valueOf(key).name());
-// query.append('=').append(properties.get(key).toString());
-// }
-// }
-
- keys: for (UserAdminConf key : UserAdminConf.values()) {
- if (key.equals(baseDn) || key.equals(uri))
- continue keys;
- Object value = properties.get(key.name());
- if (value == null)
- continue keys;
- if (first)
- first = false;
- else
- query.append('&');
- query.append(key.name());
- query.append('=').append(value.toString());
-
- }
-
- Object bDnObj = properties.get(baseDn.name());
- String bDn = bDnObj != null ? bDnObj.toString() : null;
- try {
- return new URI(null, null, bDn != null ? '/' + bDn : null, query.length() != 0 ? query.toString() : null,
- null);
- } catch (URISyntaxException e) {
- throw new UserDirectoryException("Cannot create URI from properties", e);
- }
- }
-
- public static Dictionary<String, Object> uriAsProperties(String uriStr) {
- try {
- Hashtable<String, Object> res = new Hashtable<String, Object>();
- URI u = new URI(uriStr);
- String scheme = u.getScheme();
- if (scheme != null && scheme.equals(SCHEME_IPA)) {
- return IpaUtils.convertIpaUri(u);
-// scheme = u.getScheme();
- }
- String path = u.getPath();
- // base DN
- String bDn = path.substring(path.lastIndexOf('/') + 1, path.length());
- if (bDn.equals("") && SCHEME_OS.equals(scheme)) {
- bDn = getBaseDnFromHostname();
- }
-
- if (bDn.endsWith(".ldif"))
- bDn = bDn.substring(0, bDn.length() - ".ldif".length());
-
- // Normalize base DN as LDAP name
- bDn = new LdapName(bDn).toString();
-
- String principal = null;
- String credentials = null;
- if (scheme != null)
- if (scheme.equals(SCHEME_LDAP) || scheme.equals(SCHEME_LDAPS)) {
- // TODO additional checks
- if (u.getUserInfo() != null) {
- String[] userInfo = u.getUserInfo().split(":");
- principal = userInfo.length > 0 ? userInfo[0] : null;
- credentials = userInfo.length > 1 ? userInfo[1] : null;
- }
- } else if (scheme.equals(SCHEME_FILE)) {
- } else if (scheme.equals(SCHEME_IPA)) {
- } else if (scheme.equals(SCHEME_OS)) {
- } else
- throw new UserDirectoryException("Unsupported scheme " + scheme);
- Map<String, List<String>> query = NamingUtils.queryToMap(u);
- for (String key : query.keySet()) {
- UserAdminConf ldapProp = UserAdminConf.valueOf(key);
- List<String> values = query.get(key);
- if (values.size() == 1) {
- res.put(ldapProp.name(), values.get(0));
- } else {
- throw new UserDirectoryException("Only single values are supported");
- }
- }
- res.put(baseDn.name(), bDn);
- if (SCHEME_OS.equals(scheme))
- res.put(readOnly.name(), "true");
- if (principal != null)
- res.put(Context.SECURITY_PRINCIPAL, principal);
- if (credentials != null)
- res.put(Context.SECURITY_CREDENTIALS, credentials);
- if (scheme != null) {// relative URIs are dealt with externally
- if (SCHEME_OS.equals(scheme)) {
- res.put(uri.name(), SCHEME_OS + ":///");
- } else {
- URI bareUri = new URI(scheme, null, u.getHost(), u.getPort(),
- scheme.equals(SCHEME_FILE) ? u.getPath() : null, null, null);
- res.put(uri.name(), bareUri.toString());
- }
- }
- return res;
- } catch (Exception e) {
- throw new UserDirectoryException("Cannot convert " + uri + " to properties", e);
- }
- }
-
- private static String getBaseDnFromHostname() {
- String hostname;
- try {
- hostname = InetAddress.getLocalHost().getHostName();
- } catch (UnknownHostException e) {
- hostname = "localhost.localdomain";
- }
- int dotIdx = hostname.indexOf('.');
- if (dotIdx >= 0) {
- String domain = hostname.substring(dotIdx + 1, hostname.length());
- String bDn = ("." + domain).replaceAll("\\.", ",dc=");
- bDn = bDn.substring(1, bDn.length());
- return bDn;
- } else {
- return "dc=" + hostname;
- }
- }
-
- /**
- * Hash the base DN in order to have a deterministic string to be used as a cn
- * for the underlying user directory.
- */
- public static String baseDnHash(Dictionary<String, Object> properties) {
- String bDn = (String) properties.get(baseDn.name());
- if (bDn == null)
- throw new UserDirectoryException("No baseDn in " + properties);
- return DigestUtils.sha1str(bDn);
- }
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import javax.naming.ldap.LdapName;
-import javax.transaction.xa.XAResource;
-
-/** Information about a user directory. */
-public interface UserDirectory {
- /** The base DN of all entries in this user directory */
- LdapName getBaseDn();
-
- /** The related {@link XAResource} */
- XAResource getXaResource();
-
- boolean isReadOnly();
-
- boolean isDisabled();
-
- String getUserObjectClass();
-
- String getUserBase();
-
- String getGroupObjectClass();
-
- String getGroupBase();
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import org.osgi.service.useradmin.UserAdmin;
-
-/**
- * Exceptions related to Argeo's implementation of OSGi {@link UserAdmin}
- * service.
- */
-public class UserDirectoryException extends RuntimeException {
- private static final long serialVersionUID = 1419352360062048603L;
-
- public UserDirectoryException(String message) {
- super(message);
- }
-
- public UserDirectoryException(String message, Throwable e) {
- super(message, e);
- }
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapName;
-import javax.transaction.xa.XAResource;
-
-/** {@link XAResource} for a user directory being edited. */
-class UserDirectoryWorkingCopy {
- // private final static Log log = LogFactory
- // .getLog(UserDirectoryWorkingCopy.class);
-
- private Map<LdapName, DirectoryUser> newUsers = new HashMap<LdapName, DirectoryUser>();
- private Map<LdapName, Attributes> modifiedUsers = new HashMap<LdapName, Attributes>();
- private Map<LdapName, DirectoryUser> deletedUsers = new HashMap<LdapName, DirectoryUser>();
-
- void cleanUp() {
- // clean collections
- newUsers.clear();
- newUsers = null;
- modifiedUsers.clear();
- modifiedUsers = null;
- deletedUsers.clear();
- deletedUsers = null;
- }
-
- public boolean noModifications() {
- return newUsers.size() == 0 && modifiedUsers.size() == 0
- && deletedUsers.size() == 0;
- }
-
- public Attributes getAttributes(LdapName dn) {
- if (modifiedUsers.containsKey(dn))
- return modifiedUsers.get(dn);
- return null;
- }
-
- public void startEditing(DirectoryUser user) {
- LdapName dn = user.getDn();
- if (modifiedUsers.containsKey(dn))
- throw new UserDirectoryException("Already editing " + dn);
- modifiedUsers.put(dn, (Attributes) user.getAttributes().clone());
- }
-
- public Map<LdapName, DirectoryUser> getNewUsers() {
- return newUsers;
- }
-
- public Map<LdapName, DirectoryUser> getDeletedUsers() {
- return deletedUsers;
- }
-
- public Map<LdapName, Attributes> getModifiedUsers() {
- return modifiedUsers;
- }
-}
+++ /dev/null
-package org.argeo.osgi.useradmin;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.transaction.xa.XAException;
-import javax.transaction.xa.XAResource;
-import javax.transaction.xa.Xid;
-
-/** {@link XAResource} for a user directory being edited. */
-class WcXaResource implements XAResource {
- private final AbstractUserDirectory userDirectory;
-
- private Map<Xid, UserDirectoryWorkingCopy> workingCopies = new HashMap<Xid, UserDirectoryWorkingCopy>();
- private Xid editingXid = null;
- private int transactionTimeout = 0;
-
- public WcXaResource(AbstractUserDirectory userDirectory) {
- this.userDirectory = userDirectory;
- }
-
- @Override
- public synchronized void start(Xid xid, int flags) throws XAException {
- if (editingXid != null)
- throw new UserDirectoryException("Already editing " + editingXid);
- UserDirectoryWorkingCopy wc = workingCopies.put(xid, new UserDirectoryWorkingCopy());
- if (wc != null)
- throw new UserDirectoryException("There is already a working copy for " + xid);
- this.editingXid = xid;
- }
-
- @Override
- public void end(Xid xid, int flags) throws XAException {
- checkXid(xid);
- }
-
- private UserDirectoryWorkingCopy wc(Xid xid) {
- return workingCopies.get(xid);
- }
-
- synchronized UserDirectoryWorkingCopy wc() {
- if (editingXid == null)
- return null;
- UserDirectoryWorkingCopy wc = workingCopies.get(editingXid);
- if (wc == null)
- throw new UserDirectoryException("No working copy found for " + editingXid);
- return wc;
- }
-
- private synchronized void cleanUp(Xid xid) {
- wc(xid).cleanUp();
- workingCopies.remove(xid);
- editingXid = null;
- }
-
- @Override
- public int prepare(Xid xid) throws XAException {
- checkXid(xid);
- UserDirectoryWorkingCopy wc = wc(xid);
- if (wc.noModifications())
- return XA_RDONLY;
- try {
- userDirectory.prepare(wc);
- } catch (Exception e) {
- e.printStackTrace();
- throw new XAException(XAException.XAER_RMERR);
- }
- return XA_OK;
- }
-
- @Override
- public void commit(Xid xid, boolean onePhase) throws XAException {
- try {
- checkXid(xid);
- UserDirectoryWorkingCopy wc = wc(xid);
- if (wc.noModifications())
- return;
- if (onePhase)
- userDirectory.prepare(wc);
- userDirectory.commit(wc);
- } catch (Exception e) {
- e.printStackTrace();
- throw new XAException(XAException.XAER_RMERR);
- } finally {
- cleanUp(xid);
- }
- }
-
- @Override
- public void rollback(Xid xid) throws XAException {
- try {
- checkXid(xid);
- userDirectory.rollback(wc(xid));
- } catch (Exception e) {
- e.printStackTrace();
- throw new XAException(XAException.XAER_RMERR);
- } finally {
- cleanUp(xid);
- }
- }
-
- @Override
- public void forget(Xid xid) throws XAException {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean isSameRM(XAResource xares) throws XAException {
- return xares == this;
- }
-
- @Override
- public Xid[] recover(int flag) throws XAException {
- return new Xid[0];
- }
-
- @Override
- public int getTransactionTimeout() throws XAException {
- return transactionTimeout;
- }
-
- @Override
- public boolean setTransactionTimeout(int seconds) throws XAException {
- transactionTimeout = seconds;
- return true;
- }
-
- private void checkXid(Xid xid) throws XAException {
- if (xid == null)
- throw new XAException(XAException.XAER_OUTSIDE);
- if (!xid.equals(xid))
- throw new XAException(XAException.XAER_NOTA);
- }
-
-}
+++ /dev/null
-USER_NIX {
- com.sun.security.auth.module.UnixLoginModule requisite;
-};
-
-USER_NT {
- com.sun.security.auth.module.NTLoginModule requisite;
-};
-
+++ /dev/null
-/** LDAP and LDIF based OSGi useradmin implementation. */
-package org.argeo.osgi.useradmin;
\ No newline at end of file
+++ /dev/null
-package org.argeo.osgi.util;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import org.osgi.resource.Namespace;
-import org.osgi.resource.Requirement;
-import org.osgi.resource.Resource;
-
-/** Simplify filtering resources. */
-public class FilterRequirement implements Requirement {
- private String namespace;
- private String filter;
-
- public FilterRequirement(String namespace, String filter) {
- this.namespace = namespace;
- this.filter = filter;
- }
-
- @Override
- public Resource getResource() {
- return null;
- }
-
- @Override
- public String getNamespace() {
- return namespace;
- }
-
- @Override
- public Map<String, String> getDirectives() {
- Map<String, String> directives = new HashMap<>();
- directives.put(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter);
- return directives;
- }
-
- @Override
- public Map<String, Object> getAttributes() {
- return new HashMap<>();
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.util;
-
-import java.util.concurrent.CancellationException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.function.Function;
-
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.FrameworkUtil;
-import org.osgi.framework.ServiceReference;
-import org.osgi.util.tracker.ServiceTracker;
-
-public class OnServiceRegistration<R> implements Future<R> {
- private BundleContext ownBundleContext = FrameworkUtil.getBundle(OnServiceRegistration.class).getBundleContext();
-
- private ServiceTracker<?, ?> st;
-
- private R result;
- private boolean cancelled = false;
- private Throwable exception;
-
- public <T> OnServiceRegistration(Class<T> clss, Function<T, R> function) {
- this(null, clss, function);
- }
-
- public <T> OnServiceRegistration(BundleContext bundleContext, Class<T> clss, Function<T, R> function) {
- st = new ServiceTracker<T, T>(bundleContext != null ? bundleContext : ownBundleContext, clss, null) {
-
- @Override
- public T addingService(ServiceReference<T> reference) {
- T service = super.addingService(reference);
- try {
- if (result != null)// we only want the first one
- return service;
- result = function.apply(service);
- return service;
- } catch (Exception e) {
- exception = e;
- return service;
- } finally {
- close();
- }
- }
- };
- st.open(bundleContext == null);
- }
-
- @Override
- public boolean cancel(boolean mayInterruptIfRunning) {
- if (result != null || exception != null || cancelled)
- return false;
- st.close();
- cancelled = true;
- return true;
- }
-
- @Override
- public boolean isCancelled() {
- return cancelled;
- }
-
- @Override
- public boolean isDone() {
- return result != null || cancelled;
- }
-
- @Override
- public R get() throws InterruptedException, ExecutionException {
- st.waitForService(0);
- return tryGetResult();
- }
-
- @Override
- public R get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
- st.waitForService(TimeUnit.MILLISECONDS.convert(timeout, unit));
- if (result == null)
- throw new TimeoutException("No result after " + timeout + " " + unit);
- return tryGetResult();
- }
-
- protected R tryGetResult() throws ExecutionException, CancellationException {
- if (cancelled)
- throw new CancellationException();
- if (exception != null)
- throw new ExecutionException(exception);
- if (result == null)// this should not happen
- try {
- throw new IllegalStateException("No result available");
- } catch (Exception e) {
- exception = e;
- throw new ExecutionException(e);
- }
- return result;
- }
-
-}
+++ /dev/null
-package org.argeo.osgi.util;
-
-import java.util.ArrayList;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ForkJoinPool;
-
-import org.argeo.util.register.Register;
-import org.argeo.util.register.Singleton;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceRegistration;
-
-public class OsgiRegister implements Register {
- private final BundleContext bundleContext;
- private Executor executor;
-
- private CompletableFuture<Void> shutdownStarting = new CompletableFuture<Void>();
-
- public OsgiRegister(BundleContext bundleContext) {
- this.bundleContext = bundleContext;
- // TODO experiment with dedicated executors
- this.executor = ForkJoinPool.commonPool();
- }
-
- @Override
- public <T> Singleton<T> set(T obj, Class<T> clss, Map<String, Object> attributes, Class<?>... classes) {
- CompletableFuture<ServiceRegistration<?>> srf = new CompletableFuture<ServiceRegistration<?>>();
- CompletableFuture<T> postRegistration = CompletableFuture.supplyAsync(() -> {
- List<String> lst = new ArrayList<>();
- lst.add(clss.getName());
- for (Class<?> c : classes) {
- lst.add(c.getName());
- }
- ServiceRegistration<?> sr = bundleContext.registerService(lst.toArray(new String[lst.size()]), obj,
- new Hashtable<String, Object>(attributes));
- srf.complete(sr);
- return obj;
- }, executor);
- Singleton<T> singleton = new Singleton<T>(clss, postRegistration);
-
- shutdownStarting. //
- thenCompose(singleton::prepareUnregistration). //
- thenRunAsync(() -> {
- try {
- srf.get().unregister();
- } catch (InterruptedException | ExecutionException e) {
- e.printStackTrace();
- }
- }, executor);
- return singleton;
- }
-
- public void shutdown() {
- shutdownStarting.complete(null);
- }
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Parses a CSV file interpreting the first line as a header. The
- * {@link #parse(InputStream)} method and the setters are synchronized so that
- * the object cannot be modified when parsing.
- */
-public abstract class CsvParser {
- private char separator = ',';
- private char quote = '\"';
-
- private Boolean noHeader = false;
- private Boolean strictLineAsLongAsHeader = true;
-
- /**
- * Actually process a parsed line. If
- * {@link #setStrictLineAsLongAsHeader(Boolean)} is true (default) the header
- * and the tokens are guaranteed to have the same size.
- *
- * @param lineNumber the current line number, starts at 1 (the header, if header
- * processing is enabled, the first line otherwise)
- * @param header the read-only header or null if
- * {@link #setNoHeader(Boolean)} is true (default is false)
- * @param tokens the parsed tokens
- */
- protected abstract void processLine(Integer lineNumber, List<String> header, List<String> tokens);
-
- /**
- * Parses the CSV file (stream is closed at the end)
- *
- * @param in the stream to parse
- *
- * @deprecated Use {@link #parse(InputStream, Charset)} instead.
- */
- @Deprecated
- public synchronized void parse(InputStream in) {
- parse(in, (Charset) null);
- }
-
- /**
- * Parses the CSV file (stream is closed at the end)
- *
- * @param in the stream to parse
- * @param encoding the encoding to use.
- *
- * @deprecated Use {@link #parse(InputStream, Charset)} instead.
- */
- @Deprecated
- public synchronized void parse(InputStream in, String encoding) {
- Reader reader;
- if (encoding == null)
- reader = new InputStreamReader(in);
- else
- try {
- reader = new InputStreamReader(in, encoding);
- } catch (UnsupportedEncodingException e) {
- throw new IllegalArgumentException(e);
- }
- parse(reader);
- }
-
- /**
- * Parses the CSV file (stream is closed at the end)
- *
- * @param in the stream to parse
- * @param charset the charset to use
- */
- public synchronized void parse(InputStream in, Charset charset) {
- Reader reader;
- if (charset == null)
- reader = new InputStreamReader(in);
- else
- reader = new InputStreamReader(in, charset);
- parse(reader);
- }
-
- /**
- * Parses the CSV file (stream is closed at the end)
- *
- * @param reader the reader to use (it will be buffered)
- */
- public synchronized void parse(Reader reader) {
- Integer lineCount = 0;
- try (BufferedReader bufferedReader = new BufferedReader(reader)) {
- List<String> header = null;
- if (!noHeader) {
- String headerStr = bufferedReader.readLine();
- if (headerStr == null)// empty file
- return;
- lineCount++;
- header = new ArrayList<String>();
- StringBuffer currStr = new StringBuffer("");
- Boolean wasInquote = false;
- while (parseLine(headerStr, header, currStr, wasInquote)) {
- headerStr = bufferedReader.readLine();
- if (headerStr == null)
- break;
- wasInquote = true;
- }
- header = Collections.unmodifiableList(header);
- }
-
- String line = null;
- lines: while ((line = bufferedReader.readLine()) != null) {
- line = preProcessLine(line);
- if (line == null) {
- // skip line
- continue lines;
- }
- lineCount++;
- List<String> tokens = new ArrayList<String>();
- StringBuffer currStr = new StringBuffer("");
- Boolean wasInquote = false;
- sublines: while (parseLine(line, tokens, currStr, wasInquote)) {
- line = bufferedReader.readLine();
- if (line == null)
- break sublines;
- wasInquote = true;
- }
- if (!noHeader && strictLineAsLongAsHeader) {
- int headerSize = header.size();
- int tokenSize = tokens.size();
- if (tokenSize == 1 && line.trim().equals(""))
- continue lines;// empty line
- if (headerSize != tokenSize) {
- throw new IllegalStateException("Token size " + tokenSize + " is different from header size "
- + headerSize + " at line " + lineCount + ", line: " + line + ", header: " + header
- + ", tokens: " + tokens);
- }
- }
- processLine(lineCount, header, tokens);
- }
- } catch (IOException e) {
- throw new RuntimeException("Cannot parse CSV file (line: " + lineCount + ")", e);
- }
- }
-
- /**
- * Called before each (logical) line is processed, giving a change to modify it
- * (typically for cleaning dirty files). To be overridden, return the line
- * unchanged by default. Skip the line if 'null' is returned.
- */
- protected String preProcessLine(String line) {
- return line;
- }
-
- /**
- * Parses a line character by character for performance purpose
- *
- * @return whether to continue parsing this line
- */
- protected Boolean parseLine(String str, List<String> tokens, StringBuffer currStr, Boolean wasInquote) {
- if (wasInquote)
- currStr.append('\n');
-
- char[] arr = str.toCharArray();
- boolean inQuote = wasInquote;
- for (int i = 0; i < arr.length; i++) {
- char c = arr[i];
- if (c == separator) {
- if (!inQuote) {
- tokens.add(currStr.toString());
-// currStr.delete(0, currStr.length());
- currStr.setLength(0);
- currStr.trimToSize();
- } else {
- // we don't remove separator that are in a quoted substring
- // System.out
- // .println("IN QUOTE, got a separator: [" + c + "]");
- currStr.append(c);
- }
- } else if (c == quote) {
- if (inQuote && (i + 1) < arr.length && arr[i + 1] == quote) {
- // case of double quote
- currStr.append(quote);
- i++;
- } else {// standard
- inQuote = inQuote ? false : true;
- }
- } else {
- currStr.append(c);
- }
- }
-
- if (!inQuote) {
- tokens.add(currStr.toString());
- // System.out.println("# TOKEN: " + currStr);
- }
- // if (inQuote)
- // throw new ArgeoException("Missing quote at the end of the line "
- // + str + " (parsed: " + tokens + ")");
- if (inQuote)
- return true;
- else
- return false;
- // return tokens;
- }
-
- public char getSeparator() {
- return separator;
- }
-
- public synchronized void setSeparator(char separator) {
- this.separator = separator;
- }
-
- public char getQuote() {
- return quote;
- }
-
- public synchronized void setQuote(char quote) {
- this.quote = quote;
- }
-
- public Boolean getNoHeader() {
- return noHeader;
- }
-
- public synchronized void setNoHeader(Boolean noHeader) {
- this.noHeader = noHeader;
- }
-
- public Boolean getStrictLineAsLongAsHeader() {
- return strictLineAsLongAsHeader;
- }
-
- public synchronized void setStrictLineAsLongAsHeader(Boolean strictLineAsLongAsHeader) {
- this.strictLineAsLongAsHeader = strictLineAsLongAsHeader;
- }
-
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * CSV parser allowing to process lines as maps whose keys are the header
- * fields.
- */
-public abstract class CsvParserWithLinesAsMap extends CsvParser {
-
- /**
- * Actually processes a line.
- *
- * @param lineNumber the current line number, starts at 1 (the header, if header
- * processing is enabled, the first lien otherwise)
- * @param line the parsed tokens as a map whose keys are the header fields
- */
- protected abstract void processLine(Integer lineNumber, Map<String, String> line);
-
- protected final void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
- if (header == null)
- throw new IllegalArgumentException("Only CSV with header is supported");
- Map<String, String> line = new HashMap<String, String>();
- for (int i = 0; i < header.size(); i++) {
- String key = header.get(i);
- String value = null;
- if (i < tokens.size())
- value = tokens.get(i);
- line.put(key, value);
- }
- processLine(lineNumber, line);
- }
-
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
-import java.io.Writer;
-import java.nio.charset.Charset;
-import java.util.Iterator;
-import java.util.List;
-
-/** Write in CSV format. */
-public class CsvWriter {
- private final Writer out;
-
- private char separator = ',';
- private char quote = '\"';
-
- /**
- * Creates a CSV writer.
- *
- * @param out the stream to write to. Caller is responsible for closing it.
- *
- * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead.
- *
- */
- @Deprecated
- public CsvWriter(OutputStream out) {
- this.out = new OutputStreamWriter(out);
- }
-
- /**
- * Creates a CSV writer.
- *
- * @param out the stream to write to. Caller is responsible for closing it.
- * @param encoding the encoding to use.
- *
- * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead.
- */
- @Deprecated
- public CsvWriter(OutputStream out, String encoding) {
- try {
- this.out = new OutputStreamWriter(out, encoding);
- } catch (UnsupportedEncodingException e) {
- throw new IllegalArgumentException(e);
- }
- }
-
- /**
- * Creates a CSV writer.
- *
- * @param out the stream to write to. Caller is responsible for closing it.
- * @param charset the charset to use
- */
- public CsvWriter(OutputStream out, Charset charset) {
- this.out = new OutputStreamWriter(out, charset);
- }
-
- /**
- * Creates a CSV writer.
- *
- * @param out the stream to write to. Caller is responsible for closing it.
- */
- public CsvWriter(Writer writer) {
- this.out = writer;
- }
-
- /**
- * Write a CSV line. Also used to write a header if needed (this is transparent
- * for the CSV writer): simply call it first, before writing the lines.
- */
- public void writeLine(List<?> tokens) {
- try {
- Iterator<?> it = tokens.iterator();
- while (it.hasNext()) {
- Object obj = it.next();
- writeToken(obj != null ? obj.toString() : null);
- if (it.hasNext())
- out.write(separator);
- }
- out.write('\n');
- out.flush();
- } catch (IOException e) {
- throw new RuntimeException("Could not write " + tokens, e);
- }
- }
-
- /**
- * Write a CSV line. Also used to write a header if needed (this is transparent
- * for the CSV writer): simply call it first, before writing the lines.
- */
- public void writeLine(Object[] tokens) {
- try {
- for (int i = 0; i < tokens.length; i++) {
- if (tokens[i] == null) {
- writeToken(null);
- } else {
- writeToken(tokens[i].toString());
- }
- if (i != (tokens.length - 1))
- out.write(separator);
- }
- out.write('\n');
- out.flush();
- } catch (IOException e) {
- throw new RuntimeException("Could not write " + tokens, e);
- }
- }
-
- protected void writeToken(String token) throws IOException {
- if (token == null) {
- // TODO configure how to deal with null
- out.write("");
- return;
- }
- // +2 for possible quotes, another +2 assuming there would be an already
- // quoted string where quotes needs to be duplicated
- // another +2 for safety
- // we don't want to increase buffer size while writing
- StringBuffer buf = new StringBuffer(token.length() + 6);
- char[] arr = token.toCharArray();
- boolean shouldQuote = false;
- for (char c : arr) {
- if (!shouldQuote) {
- if (c == separator)
- shouldQuote = true;
- if (c == '\n')
- shouldQuote = true;
- }
-
- if (c == quote) {
- shouldQuote = true;
- // duplicate quote
- buf.append(quote);
- }
-
- // generic case
- buf.append(c);
- }
-
- if (shouldQuote == true)
- out.write(quote);
- out.write(buf.toString());
- if (shouldQuote == true)
- out.write(quote);
- }
-
- public void setSeparator(char separator) {
- this.separator = separator;
- }
-
- public void setQuote(char quote) {
- this.quote = quote;
- }
-
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.Iterator;
-
-/**
- * Access the keys of a {@link String}-keyed {@link Dictionary} (common throughout
- * the OSGi APIs) as an {@link Iterable} so that they are easily usable in
- * for-each loops.
- */
-class DictionaryKeys implements Iterable<String> {
- private final Dictionary<String, ?> dictionary;
-
- public DictionaryKeys(Dictionary<String, ?> dictionary) {
- this.dictionary = dictionary;
- }
-
- @Override
- public Iterator<String> iterator() {
- return new KeyIterator(dictionary.keys());
- }
-
- private static class KeyIterator implements Iterator<String> {
- private final Enumeration<String> keys;
-
- KeyIterator(Enumeration<String> keys) {
- this.keys = keys;
- }
-
- @Override
- public boolean hasNext() {
- return keys.hasMoreElements();
- }
-
- @Override
- public String next() {
- return keys.nextElement();
- }
-
- }
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.channels.FileChannel.MapMode;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-/** Utilities around cryptographic digests */
-public class DigestUtils {
- public final static String MD5 = "MD5";
- public final static String SHA1 = "SHA1";
- public final static String SHA256 = "SHA-256";
- public final static String SHA512 = "SHA-512";
-
- private static Boolean debug = false;
- // TODO: make it configurable
- private final static Integer byteBufferCapacity = 100 * 1024;// 100 KB
-
- public static byte[] sha1(byte[] bytes) {
- try {
- MessageDigest digest = MessageDigest.getInstance(SHA1);
- digest.update(bytes);
- byte[] checksum = digest.digest();
- return checksum;
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalArgumentException(e);
- }
- }
-
- public static String digest(String algorithm, byte[] bytes) {
- try {
- MessageDigest digest = MessageDigest.getInstance(algorithm);
- digest.update(bytes);
- byte[] checksum = digest.digest();
- String res = encodeHexString(checksum);
- return res;
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
- }
- }
-
- public static String digest(String algorithm, InputStream in) {
- try {
- MessageDigest digest = MessageDigest.getInstance(algorithm);
- // ReadableByteChannel channel = Channels.newChannel(in);
- // ByteBuffer bb = ByteBuffer.allocateDirect(byteBufferCapacity);
- // while (channel.read(bb) > 0)
- // digest.update(bb);
- byte[] buffer = new byte[byteBufferCapacity];
- int read = 0;
- while ((read = in.read(buffer)) > 0) {
- digest.update(buffer, 0, read);
- }
-
- byte[] checksum = digest.digest();
- String res = encodeHexString(checksum);
- return res;
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
- } catch (IOException e) {
- throw new RuntimeException(e);
- } finally {
- StreamUtils.closeQuietly(in);
- }
- }
-
- public static String digest(String algorithm, File file) {
- FileInputStream fis = null;
- FileChannel fc = null;
- try {
- fis = new FileInputStream(file);
- fc = fis.getChannel();
-
- // Get the file's size and then map it into memory
- int sz = (int) fc.size();
- ByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, sz);
- return digest(algorithm, bb);
- } catch (IOException e) {
- throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e);
- } finally {
- StreamUtils.closeQuietly(fis);
- if (fc.isOpen())
- try {
- fc.close();
- } catch (IOException e) {
- // silent
- }
- }
- }
-
- protected static String digest(String algorithm, ByteBuffer bb) {
- long begin = System.currentTimeMillis();
- try {
- MessageDigest digest = MessageDigest.getInstance(algorithm);
- digest.update(bb);
- byte[] checksum = digest.digest();
- String res = encodeHexString(checksum);
- long end = System.currentTimeMillis();
- if (debug)
- System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s");
- return res;
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
- }
- }
-
- public static String sha1hex(Path path) {
- return digest(SHA1, path, byteBufferCapacity);
- }
-
- public static String digest(String algorithm, Path path, long bufferSize) {
- byte[] digest = digestRaw(algorithm, path, bufferSize);
- return encodeHexString(digest);
- }
-
- public static byte[] digestRaw(String algorithm, Path file, long bufferSize) {
- long begin = System.currentTimeMillis();
- try {
- MessageDigest md = MessageDigest.getInstance(algorithm);
- FileChannel fc = FileChannel.open(file);
- long fileSize = Files.size(file);
- if (fileSize <= bufferSize) {
- ByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, fileSize);
- md.update(bb);
- } else {
- long lastCycle = (fileSize / bufferSize) - 1;
- long position = 0;
- for (int i = 0; i <= lastCycle; i++) {
- ByteBuffer bb;
- if (i != lastCycle) {
- bb = fc.map(MapMode.READ_ONLY, position, bufferSize);
- position = position + bufferSize;
- } else {
- bb = fc.map(MapMode.READ_ONLY, position, fileSize - position);
- position = fileSize;
- }
- md.update(bb);
- }
- }
- long end = System.currentTimeMillis();
- if (debug)
- System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s");
- return md.digest();
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e);
- } catch (IOException e) {
- throw new RuntimeException("Cannot digest " + file + " with algorithm " + algorithm, e);
- }
- }
-
- public static void main(String[] args) {
- File file;
- if (args.length > 0)
- file = new File(args[0]);
- else {
- System.err.println("Usage: <file> [<algorithm>]" + " (see http://java.sun.com/j2se/1.5.0/"
- + "docs/guide/security/CryptoSpec.html#AppA)");
- return;
- }
-
- if (args.length > 1) {
- String algorithm = args[1];
- System.out.println(digest(algorithm, file));
- } else {
- String algorithm = "MD5";
- System.out.println(algorithm + ": " + digest(algorithm, file));
- algorithm = "SHA";
- System.out.println(algorithm + ": " + digest(algorithm, file));
- System.out.println(algorithm + ": " + sha1hex(file.toPath()));
- algorithm = "SHA-256";
- System.out.println(algorithm + ": " + digest(algorithm, file));
- algorithm = "SHA-512";
- System.out.println(algorithm + ": " + digest(algorithm, file));
- }
- }
-
- final private static char[] hexArray = "0123456789abcdef".toCharArray();
-
- /**
- * From
- * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
- * -a-hex-string-in-java
- */
- public static String encodeHexString(byte[] bytes) {
- char[] hexChars = new char[bytes.length * 2];
- for (int j = 0; j < bytes.length; j++) {
- int v = bytes[j] & 0xFF;
- hexChars[j * 2] = hexArray[v >>> 4];
- hexChars[j * 2 + 1] = hexArray[v & 0x0F];
- }
- return new String(hexChars);
- }
-
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.io.IOException;
-import java.io.PrintStream;
-import java.nio.charset.Charset;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/** Hashes the hashes of the files in a directory. */
-public class DirH {
-
- private final static Charset charset = Charset.forName("UTF-16");
- private final static long bufferSize = 200 * 1024 * 1024;
- private final static String algorithm = "SHA";
-
- private final static byte EOL = (byte) '\n';
- private final static byte SPACE = (byte) ' ';
-
- private final int hashSize;
-
- private final byte[][] hashes;
- private final byte[][] fileNames;
- private final byte[] digest;
- private final byte[] dirName;
-
- /**
- * @param dirName can be null or empty
- */
- private DirH(byte[][] hashes, byte[][] fileNames, byte[] dirName) {
- if (hashes.length != fileNames.length)
- throw new IllegalArgumentException(hashes.length + " hashes and " + fileNames.length + " file names");
- this.hashes = hashes;
- this.fileNames = fileNames;
- this.dirName = dirName == null ? new byte[0] : dirName;
- if (hashes.length == 0) {// empty dir
- hashSize = 20;
- // FIXME what is the digest of an empty dir?
- digest = new byte[hashSize];
- Arrays.fill(digest, SPACE);
- return;
- }
- hashSize = hashes[0].length;
- for (int i = 0; i < hashes.length; i++) {
- if (hashes[i].length != hashSize)
- throw new IllegalArgumentException(
- "Hash size for " + new String(fileNames[i], charset) + " is " + hashes[i].length);
- }
-
- try {
- MessageDigest md = MessageDigest.getInstance(algorithm);
- for (int i = 0; i < hashes.length; i++) {
- md.update(this.hashes[i]);
- md.update(SPACE);
- md.update(this.fileNames[i]);
- md.update(EOL);
- }
- digest = md.digest();
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalArgumentException("Cannot digest", e);
- }
- }
-
- public void print(PrintStream out) {
- out.print(DigestUtils.encodeHexString(digest));
- if (dirName.length > 0) {
- out.print(' ');
- out.print(new String(dirName, charset));
- }
- out.print('\n');
- for (int i = 0; i < hashes.length; i++) {
- out.print(DigestUtils.encodeHexString(hashes[i]));
- out.print(' ');
- out.print(new String(fileNames[i], charset));
- out.print('\n');
- }
- }
-
- public static DirH digest(Path dir) {
- try (DirectoryStream<Path> files = Files.newDirectoryStream(dir)) {
- List<byte[]> hs = new ArrayList<byte[]>();
- List<String> fNames = new ArrayList<>();
- for (Path file : files) {
- if (!Files.isDirectory(file)) {
- byte[] digest = DigestUtils.digestRaw(algorithm, file, bufferSize);
- hs.add(digest);
- fNames.add(file.getFileName().toString());
- }
- }
-
- byte[][] fileNames = new byte[fNames.size()][];
- for (int i = 0; i < fNames.size(); i++) {
- fileNames[i] = fNames.get(i).getBytes(charset);
- }
- byte[][] hashes = hs.toArray(new byte[hs.size()][]);
- return new DirH(hashes, fileNames, dir.toString().getBytes(charset));
- } catch (IOException e) {
- throw new RuntimeException("Cannot digest " + dir, e);
- }
- }
-
- public static void main(String[] args) {
- try {
- DirH dirH = DirH.digest(Paths.get("/home/mbaudier/tmp/"));
- dirH.print(System.out);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.Writer;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.time.ZonedDateTime;
-import java.time.temporal.ChronoUnit;
-import java.time.temporal.Temporal;
-import java.util.ArrayList;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-
-import javax.naming.InvalidNameException;
-import javax.naming.ldap.LdapName;
-
-/** Utilities around Java basic features. */
-public class LangUtils {
- /*
- * NON-API OSGi
- */
- /**
- * Returns an array with the names of the provided classes. Useful when
- * registering services with multiple interfaces in OSGi.
- */
- public static String[] names(Class<?>... clzz) {
- String[] res = new String[clzz.length];
- for (int i = 0; i < clzz.length; i++)
- res[i] = clzz[i].getName();
- return res;
- }
-
- /*
- * MAP
- */
- /**
- * Creates a new {@link Dictionary} with one key-value pair. Key should not be
- * null, but if the value is null, it returns an empty {@link Dictionary}.
- */
- public static Map<String, Object> map(String key, Object value) {
- assert key != null;
- HashMap<String, Object> props = new HashMap<>();
- if (value != null)
- props.put(key, value);
- return props;
- }
-
- /*
- * DICTIONARY
- */
-
- /**
- * Creates a new {@link Dictionary} with one key-value pair. Key should not be
- * null, but if the value is null, it returns an empty {@link Dictionary}.
- */
- public static Dictionary<String, Object> dict(String key, Object value) {
- assert key != null;
- Hashtable<String, Object> props = new Hashtable<>();
- if (value != null)
- props.put(key, value);
- return props;
- }
-
- /** @deprecated Use {@link #dict(String, Object)} instead. */
- @Deprecated
- public static Dictionary<String, Object> dico(String key, Object value) {
- return dict(key, value);
- }
-
- /** Converts a {@link Dictionary} to a {@link Map} of strings. */
- public static Map<String, String> dictToStringMap(Dictionary<String, ?> properties) {
- if (properties == null) {
- return null;
- }
- Map<String, String> res = new HashMap<>(properties.size());
- Enumeration<String> keys = properties.keys();
- while (keys.hasMoreElements()) {
- String key = keys.nextElement();
- res.put(key, properties.get(key).toString());
- }
- return res;
- }
-
- /**
- * Get a string property from this map, expecting to find it, or
- * <code>null</code> if not found.
- */
- public static String get(Map<String, ?> map, String key) {
- Object res = map.get(key);
- if (res == null)
- return null;
- return res.toString();
- }
-
- /**
- * Get a string property from this map, expecting to find it.
- *
- * @throws IllegalArgumentException if the key was not found
- */
- public static String getNotNull(Map<String, ?> map, String key) {
- Object res = map.get(key);
- if (res == null)
- throw new IllegalArgumentException("Map " + map + " should contain key " + key);
- return res.toString();
- }
-
- /**
- * Wraps the keys of the provided {@link Dictionary} as an {@link Iterable}.
- */
- public static Iterable<String> keys(Dictionary<String, ?> props) {
- assert props != null;
- return new DictionaryKeys(props);
- }
-
- static String toJson(Dictionary<String, ?> props) {
- return toJson(props, false);
- }
-
- static String toJson(Dictionary<String, ?> props, boolean pretty) {
- StringBuilder sb = new StringBuilder();
- sb.append('{');
- if (pretty)
- sb.append('\n');
- Enumeration<String> keys = props.keys();
- while (keys.hasMoreElements()) {
- String key = keys.nextElement();
- if (pretty)
- sb.append(' ');
- sb.append('\"').append(key).append('\"');
- if (pretty)
- sb.append(" : ");
- else
- sb.append(':');
- sb.append('\"').append(props.get(key)).append('\"');
- if (keys.hasMoreElements())
- sb.append(", ");
- if (pretty)
- sb.append('\n');
- }
- sb.append('}');
- return sb.toString();
- }
-
- static void storeAsProperties(Dictionary<String, Object> props, Path path) throws IOException {
- if (props == null)
- throw new IllegalArgumentException("Props cannot be null");
- Properties toStore = new Properties();
- for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
- String key = keys.nextElement();
- toStore.setProperty(key, props.get(key).toString());
- }
- try (OutputStream out = Files.newOutputStream(path)) {
- toStore.store(out, null);
- }
- }
-
- static void appendAsLdif(String dnBase, String dnKey, Dictionary<String, Object> props, Path path)
- throws IOException {
- if (props == null)
- throw new IllegalArgumentException("Props cannot be null");
- Object dnValue = props.get(dnKey);
- String dnStr = dnKey + '=' + dnValue + ',' + dnBase;
- LdapName dn;
- try {
- dn = new LdapName(dnStr);
- } catch (InvalidNameException e) {
- throw new IllegalArgumentException("Cannot interpret DN " + dnStr, e);
- }
- if (dnValue == null)
- throw new IllegalArgumentException("DN key " + dnKey + " must have a value");
- try (Writer writer = Files.newBufferedWriter(path, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) {
- writer.append("\ndn: ");
- writer.append(dn.toString());
- writer.append('\n');
- for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
- String key = keys.nextElement();
- Object value = props.get(key);
- writer.append(key);
- writer.append(": ");
- // FIXME deal with binary and multiple values
- writer.append(value.toString());
- writer.append('\n');
- }
- }
- }
-
- static Dictionary<String, Object> loadFromProperties(Path path) throws IOException {
- Properties toLoad = new Properties();
- try (InputStream in = Files.newInputStream(path)) {
- toLoad.load(in);
- }
- Dictionary<String, Object> res = new Hashtable<String, Object>();
- for (Object key : toLoad.keySet())
- res.put(key.toString(), toLoad.get(key));
- return res;
- }
-
- /*
- * COLLECTIONS
- */
- /**
- * Convert a comma-separated separated {@link String} or a {@link String} array
- * to a {@link List} of {@link String}, trimming them. Useful to quickly
- * interpret OSGi services properties.
- *
- * @return a {@link List} containing the trimmed {@link String}s, or an empty
- * {@link List} if the argument was <code>null</code>.
- */
- public static List<String> toStringList(Object value) {
- List<String> values = new ArrayList<>();
- if (value == null)
- return values;
- String[] arr;
- if (value instanceof String) {
- arr = ((String) value).split(",");
- } else if (value instanceof String[]) {
- arr = (String[]) value;
- } else {
- throw new IllegalArgumentException("Unsupported value type " + value.getClass());
- }
- for (String str : arr) {
- values.add(str.trim());
- }
- return values;
- }
-
- /*
- * EXCEPTIONS
- */
- /**
- * Chain the messages of all causes (one per line, <b>starts with a line
- * return</b>) without all the stack
- */
- public static String chainCausesMessages(Throwable t) {
- StringBuffer buf = new StringBuffer();
- chainCauseMessage(buf, t);
- return buf.toString();
- }
-
- /** Recursive chaining of messages */
- private static void chainCauseMessage(StringBuffer buf, Throwable t) {
- buf.append('\n').append(' ').append(t.getClass().getCanonicalName()).append(": ").append(t.getMessage());
- if (t.getCause() != null)
- chainCauseMessage(buf, t.getCause());
- }
-
- /*
- * TIME
- */
- /** Formats time elapsed since start. */
- public static String since(ZonedDateTime start) {
- ZonedDateTime now = ZonedDateTime.now();
- return duration(start, now);
- }
-
- /** Formats a duration. */
- public static String duration(Temporal start, Temporal end) {
- long count = ChronoUnit.DAYS.between(start, end);
- if (count != 0)
- return count > 1 ? count + " days" : count + " day";
- count = ChronoUnit.HOURS.between(start, end);
- if (count != 0)
- return count > 1 ? count + " hours" : count + " hours";
- count = ChronoUnit.MINUTES.between(start, end);
- if (count != 0)
- return count > 1 ? count + " minutes" : count + " minute";
- count = ChronoUnit.SECONDS.between(start, end);
- return count > 1 ? count + " seconds" : count + " second";
- }
-
- /** Singleton constructor. */
- private LangUtils() {
-
- }
-
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.io.File;
-import java.lang.management.ManagementFactory;
-
-/** When OS specific informations are needed. */
-public class OS {
- public final static OS LOCAL = new OS();
-
- private final String arch, name, version;
-
- /** The OS of the running JVM */
- protected OS() {
- arch = System.getProperty("os.arch");
- name = System.getProperty("os.name");
- version = System.getProperty("os.version");
- }
-
- public String getArch() {
- return arch;
- }
-
- public String getName() {
- return name;
- }
-
- public String getVersion() {
- return version;
- }
-
- public boolean isMSWindows() {
- // only MS Windows would use such an horrendous separator...
- return File.separatorChar == '\\';
- }
-
- public String[] getDefaultShellCommand() {
- if (!isMSWindows())
- return new String[] { "/bin/sh", "-l", "-i" };
- else
- return new String[] { "cmd.exe", "/C" };
- }
-
- public static Integer getJvmPid() {
- /*
- * This method works on most platforms (including Linux). Although when Java 9
- * comes along, there is a better way: long pid =
- * ProcessHandle.current().getPid();
- *
- * See:
- * http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-
- * process-id
- */
- String pidAndHost = ManagementFactory.getRuntimeMXBean().getName();
- return Integer.parseInt(pidAndHost.substring(0, pidAndHost.indexOf('@')));
- }
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
-import java.security.Key;
-
-import javax.crypto.Cipher;
-import javax.crypto.CipherInputStream;
-import javax.crypto.CipherOutputStream;
-import javax.crypto.SecretKey;
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.PBEKeySpec;
-import javax.crypto.spec.SecretKeySpec;
-
-public class PasswordEncryption {
- public final static Integer DEFAULT_ITERATION_COUNT = 1024;
- /** Stronger with 256, but causes problem with Oracle JVM */
- public final static Integer DEFAULT_SECRETE_KEY_LENGTH = 256;
- public final static Integer DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED = 128;
- public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1";
- public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES";
- public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding";
-// public final static String DEFAULT_CHARSET = "UTF-8";
- public final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
-
- private Integer iterationCount = DEFAULT_ITERATION_COUNT;
- private Integer secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH;
- private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY;
- private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION;
- private String cipherName = DEFAULT_CIPHER_NAME;
-
- private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
- (byte) 0x35, (byte) 0xE3, (byte) 0x03 };
- private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
- (byte) 0x35, (byte) 0xE3, (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
- (byte) 0x35, (byte) 0xE3, (byte) 0x03 };
-
- private Key key;
- private Cipher ecipher;
- private Cipher dcipher;
-
- private String securityProviderName = null;
-
- /**
- * This is up to the caller to clear the passed array. Neither copy of nor
- * reference to the passed array is kept
- */
- public PasswordEncryption(char[] password) {
- this(password, DEFAULT_SALT_8, DEFAULT_IV_16);
- }
-
- /**
- * This is up to the caller to clear the passed array. Neither copies of nor
- * references to the passed arrays are kept
- */
- public PasswordEncryption(char[] password, byte[] passwordSalt, byte[] initializationVector) {
- try {
- initKeyAndCiphers(password, passwordSalt, initializationVector);
- } catch (InvalidKeyException e) {
- Integer previousSecreteKeyLength = secreteKeyLength;
- secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED;
- System.err.println("'" + e.getMessage() + "', will use " + secreteKeyLength
- + " secrete key length instead of " + previousSecreteKeyLength);
- try {
- initKeyAndCiphers(password, passwordSalt, initializationVector);
- } catch (GeneralSecurityException e1) {
- throw new IllegalStateException("Cannot get secret key (with restricted length)", e1);
- }
- } catch (GeneralSecurityException e) {
- throw new IllegalStateException("Cannot get secret key", e);
- }
- }
-
- protected void initKeyAndCiphers(char[] password, byte[] passwordSalt, byte[] initializationVector)
- throws GeneralSecurityException {
- byte[] salt = new byte[8];
- System.arraycopy(passwordSalt, 0, salt, 0, salt.length);
- // for (int i = 0; i < password.length && i < salt.length; i++)
- // salt[i] = (byte) password[i];
- byte[] iv = new byte[16];
- System.arraycopy(initializationVector, 0, iv, 0, iv.length);
-
- SecretKeyFactory keyFac = SecretKeyFactory.getInstance(getSecretKeyFactoryName());
- PBEKeySpec keySpec = new PBEKeySpec(password, salt, getIterationCount(), getKeyLength());
- String secKeyEncryption = getSecretKeyEncryption();
- if (secKeyEncryption != null) {
- SecretKey tmp = keyFac.generateSecret(keySpec);
- key = new SecretKeySpec(tmp.getEncoded(), getSecretKeyEncryption());
- } else {
- key = keyFac.generateSecret(keySpec);
- }
- if (securityProviderName != null)
- ecipher = Cipher.getInstance(getCipherName(), securityProviderName);
- else
- ecipher = Cipher.getInstance(getCipherName());
- ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
- dcipher = Cipher.getInstance(getCipherName());
- dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
- }
-
- public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) throws IOException {
- try {
- CipherOutputStream out = new CipherOutputStream(encryptedOut, ecipher);
- StreamUtils.copy(decryptedIn, out);
- StreamUtils.closeQuietly(out);
- } catch (IOException e) {
- throw e;
- } finally {
- StreamUtils.closeQuietly(decryptedIn);
- }
- }
-
- public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) throws IOException {
- try {
- CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, dcipher);
- StreamUtils.copy(decryptedIn, decryptedOut);
- } catch (IOException e) {
- throw e;
- } finally {
- StreamUtils.closeQuietly(encryptedIn);
- }
- }
-
- public byte[] encryptString(String str) {
- ByteArrayOutputStream out = null;
- ByteArrayInputStream in = null;
- try {
- out = new ByteArrayOutputStream();
- in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET));
- encrypt(in, out);
- return out.toByteArray();
- } catch (IOException e) {
- throw new RuntimeException(e);
- } finally {
- StreamUtils.closeQuietly(out);
- }
- }
-
- /** Closes the input stream */
- public String decryptAsString(InputStream in) {
- ByteArrayOutputStream out = null;
- try {
- out = new ByteArrayOutputStream();
- decrypt(in, out);
- return new String(out.toByteArray(), DEFAULT_CHARSET);
- } catch (IOException e) {
- throw new RuntimeException(e);
- } finally {
- StreamUtils.closeQuietly(out);
- }
- }
-
- protected Key getKey() {
- return key;
- }
-
- protected Cipher getEcipher() {
- return ecipher;
- }
-
- protected Cipher getDcipher() {
- return dcipher;
- }
-
- protected Integer getIterationCount() {
- return iterationCount;
- }
-
- protected Integer getKeyLength() {
- return secreteKeyLength;
- }
-
- protected String getSecretKeyFactoryName() {
- return secreteKeyFactoryName;
- }
-
- protected String getSecretKeyEncryption() {
- return secreteKeyEncryption;
- }
-
- protected String getCipherName() {
- return cipherName;
- }
-
- public void setIterationCount(Integer iterationCount) {
- this.iterationCount = iterationCount;
- }
-
- public void setSecreteKeyLength(Integer keyLength) {
- this.secreteKeyLength = keyLength;
- }
-
- public void setSecreteKeyFactoryName(String secreteKeyFactoryName) {
- this.secreteKeyFactoryName = secreteKeyFactoryName;
- }
-
- public void setSecreteKeyEncryption(String secreteKeyEncryption) {
- this.secreteKeyEncryption = secreteKeyEncryption;
- }
-
- public void setCipherName(String cipherName) {
- this.cipherName = cipherName;
- }
-
- public void setSecurityProviderName(String securityProviderName) {
- this.securityProviderName = securityProviderName;
- }
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.AsynchronousByteChannel;
-import java.nio.channels.CompletionHandler;
-import java.nio.channels.ReadableByteChannel;
-import java.nio.channels.WritableByteChannel;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-
-/** An {@link AsynchronousByteChannel} based on an {@link ExecutorService}. */
-public class ServiceChannel implements AsynchronousByteChannel {
- private final ReadableByteChannel in;
- private final WritableByteChannel out;
-
- private boolean open = true;
-
- private ExecutorService executor;
-
- public ServiceChannel(ReadableByteChannel in, WritableByteChannel out, ExecutorService executor) {
- this.in = in;
- this.out = out;
- this.executor = executor;
- }
-
- @Override
- public Future<Integer> read(ByteBuffer dst) {
- return executor.submit(() -> in.read(dst));
- }
-
- @Override
- public <A> void read(ByteBuffer dst, A attachment, CompletionHandler<Integer, ? super A> handler) {
- try {
- Future<Integer> res = read(dst);
- handler.completed(res.get(), attachment);
- } catch (Exception e) {
- handler.failed(e, attachment);
- }
- }
-
- @Override
- public Future<Integer> write(ByteBuffer src) {
- return executor.submit(() -> out.write(src));
- }
-
- @Override
- public <A> void write(ByteBuffer src, A attachment, CompletionHandler<Integer, ? super A> handler) {
- try {
- Future<Integer> res = write(src);
- handler.completed(res.get(), attachment);
- } catch (Exception e) {
- handler.failed(e, attachment);
- }
- }
-
- @Override
- public synchronized void close() throws IOException {
- try {
- in.close();
- } catch (Exception e) {
- e.printStackTrace();
- }
- try {
- out.close();
- } catch (Exception e) {
- e.printStackTrace();
- }
- open = false;
- notifyAll();
- }
-
- @Override
- public synchronized boolean isOpen() {
- return open;
- }
-
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.io.Writer;
-
-/** Utilities to be used when Apache Commons IO is not available. */
-class StreamUtils {
- private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
-
- /*
- * APACHE COMMONS IO (inspired)
- */
-
- /** @return the number of bytes */
- public static Long copy(InputStream in, OutputStream out)
- throws IOException {
- Long count = 0l;
- byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
- while (true) {
- int length = in.read(buf);
- if (length < 0)
- break;
- out.write(buf, 0, length);
- count = count + length;
- }
- return count;
- }
-
- /** @return the number of chars */
- public static Long copy(Reader in, Writer out) throws IOException {
- Long count = 0l;
- char[] buf = new char[DEFAULT_BUFFER_SIZE];
- while (true) {
- int length = in.read(buf);
- if (length < 0)
- break;
- out.write(buf, 0, length);
- count = count + length;
- }
- return count;
- }
-
- public static void closeQuietly(InputStream in) {
- if (in != null)
- try {
- in.close();
- } catch (Exception e) {
- //
- }
- }
-
- public static void closeQuietly(OutputStream out) {
- if (out != null)
- try {
- out.close();
- } catch (Exception e) {
- //
- }
- }
-
- public static void closeQuietly(Reader in) {
- if (in != null)
- try {
- in.close();
- } catch (Exception e) {
- //
- }
- }
-
- public static void closeQuietly(Writer out) {
- if (out != null)
- try {
- out.close();
- } catch (Exception e) {
- //
- }
- }
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-/** A generic tester based on Java assertions and functional programming. */
-public class Tester {
- private Map<String, TesterStatus> results = Collections.synchronizedSortedMap(new TreeMap<>());
-
- private ClassLoader classLoader;
-
- /** Use {@link Thread#getContextClassLoader()} by default. */
- public Tester() {
- this(Thread.currentThread().getContextClassLoader());
- }
-
- public Tester(ClassLoader classLoader) {
- this.classLoader = classLoader;
- }
-
- public void execute(String className) {
- Class<?> clss;
- try {
- clss = classLoader.loadClass(className);
- boolean assertionsEnabled = clss.desiredAssertionStatus();
- if (!assertionsEnabled)
- throw new IllegalStateException("Test runner " + getClass().getName()
- + " requires Java assertions to be enabled. Call the JVM with the -ea argument.");
- } catch (Exception e1) {
- throw new IllegalArgumentException("Cannot initalise test for " + className, e1);
-
- }
- List<Method> methods = findMethods(clss);
- if (methods.size() == 0)
- throw new IllegalArgumentException("No test method found in " + clss);
- // TODO make order more predictable?
- for (Method method : methods) {
- String uid = method.getDeclaringClass().getName() + "#" + method.getName();
- TesterStatus testStatus = new TesterStatus(uid);
- Object obj = null;
- try {
- beforeTest(uid, method);
- obj = clss.getDeclaredConstructor().newInstance();
- method.invoke(obj);
- testStatus.setPassed();
- afterTestPassed(uid, method, obj);
- } catch (Exception e) {
- testStatus.setFailed(e);
- afterTestFailed(uid, method, obj, e);
- } finally {
- results.put(uid, testStatus);
- }
- }
- }
-
- protected void beforeTest(String uid, Method method) {
- // System.out.println(uid + ": STARTING");
- }
-
- protected void afterTestPassed(String uid, Method method, Object obj) {
- System.out.println(uid + ": PASSED");
- }
-
- protected void afterTestFailed(String uid, Method method, Object obj, Throwable e) {
- System.out.println(uid + ": FAILED");
- e.printStackTrace();
- }
-
- protected List<Method> findMethods(Class<?> clss) {
- List<Method> methods = new ArrayList<Method>();
-// Method call = getMethod(clss, "call");
-// if (call != null)
-// methods.add(call);
-//
- for (Method method : clss.getMethods()) {
- if (method.getName().startsWith("test")) {
- methods.add(method);
- }
- }
- return methods;
- }
-
- protected Method getMethod(Class<?> clss, String name, Class<?>... parameterTypes) {
- try {
- return clss.getMethod(name, parameterTypes);
- } catch (NoSuchMethodException e) {
- return null;
- } catch (SecurityException e) {
- throw new IllegalStateException(e);
- }
- }
-
- public static void main(String[] args) {
- // deal with arguments
- String className;
- if (args.length < 1) {
- System.err.println(usage());
- System.exit(1);
- throw new IllegalArgumentException();
- } else {
- className = args[0];
- }
-
- Tester test = new Tester();
- try {
- test.execute(className);
- } catch (Throwable e) {
- e.printStackTrace();
- }
-
- Map<String, TesterStatus> r = test.results;
- for (String uid : r.keySet()) {
- TesterStatus testStatus = r.get(uid);
- System.out.println(testStatus);
- }
- }
-
- public static String usage() {
- return "java " + Tester.class.getName() + " [test class name]";
-
- }
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.io.Serializable;
-
-/** The status of a test. */
-public class TesterStatus implements Serializable {
- private static final long serialVersionUID = 6272975746885487000L;
-
- private Boolean passed = null;
- private final String uid;
- private Throwable throwable = null;
-
- public TesterStatus(String uid) {
- this.uid = uid;
- }
-
- /** For cloning. */
- public TesterStatus(String uid, Boolean passed, Throwable throwable) {
- this(uid);
- this.passed = passed;
- this.throwable = throwable;
- }
-
- public synchronized Boolean isRunning() {
- return passed == null;
- }
-
- public synchronized Boolean isPassed() {
- assert passed != null;
- return passed;
- }
-
- public synchronized Boolean isFailed() {
- assert passed != null;
- return !passed;
- }
-
- public synchronized void setPassed() {
- setStatus(true);
- }
-
- public synchronized void setFailed() {
- setStatus(false);
- }
-
- public synchronized void setFailed(Throwable throwable) {
- setStatus(false);
- setThrowable(throwable);
- }
-
- protected void setStatus(Boolean passed) {
- if (this.passed != null)
- throw new IllegalStateException("Passed status of test " + uid + " is already set (to " + passed + ")");
- this.passed = passed;
- }
-
- protected void setThrowable(Throwable throwable) {
- if (this.throwable != null)
- throw new IllegalStateException("Throwable of test " + uid + " is already set (to " + passed + ")");
- this.throwable = throwable;
- }
-
- public String getUid() {
- return uid;
- }
-
- public Throwable getThrowable() {
- return throwable;
- }
-
- @Override
- protected Object clone() throws CloneNotSupportedException {
- // TODO Auto-generated method stub
- return super.clone();
- }
-
- @Override
- public boolean equals(Object o) {
- if (o instanceof TesterStatus) {
- TesterStatus other = (TesterStatus) o;
- // we don't check consistency for performance purposes
- // this equals() is supposed to be used in collections or for transfer
- return other.uid.equals(uid);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return uid.hashCode();
- }
-
- @Override
- public String toString() {
- return uid + "\t" + (passed ? "passed" : "failed");
- }
-
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.text.NumberFormat;
-import java.text.ParseException;
-import java.util.Locale;
-
-/** A throughput, that is, a value per unit of time. */
-public class Throughput {
- private final static NumberFormat usNumberFormat = NumberFormat.getInstance(Locale.US);
-
- public enum Unit {
- s, m, h, d
- }
-
- private final Double value;
- private final Unit unit;
-
- public Throughput(Double value, Unit unit) {
- this.value = value;
- this.unit = unit;
- }
-
- public Throughput(Long periodMs, Long count, Unit unit) {
- if (unit.equals(Unit.s))
- value = ((double) count * 1000d) / periodMs;
- else if (unit.equals(Unit.m))
- value = ((double) count * 60d * 1000d) / periodMs;
- else if (unit.equals(Unit.h))
- value = ((double) count * 60d * 60d * 1000d) / periodMs;
- else if (unit.equals(Unit.d))
- value = ((double) count * 24d * 60d * 60d * 1000d) / periodMs;
- else
- throw new IllegalArgumentException("Unsupported unit " + unit);
- this.unit = unit;
- }
-
- public Throughput(Double value, String unitStr) {
- this(value, Unit.valueOf(unitStr));
- }
-
- public Throughput(String def) {
- int index = def.indexOf('/');
- if (def.length() < 3 || index <= 0 || index != def.length() - 2)
- throw new IllegalArgumentException(
- def + " no a proper throughput definition" + " (should be <value>/<unit>, e.g. 3.54/s or 1500/h");
- String valueStr = def.substring(0, index);
- String unitStr = def.substring(index + 1);
- try {
- this.value = usNumberFormat.parse(valueStr).doubleValue();
- } catch (ParseException e) {
- throw new IllegalArgumentException("Cannot parse " + valueStr + " as a number.", e);
- }
- this.unit = Unit.valueOf(unitStr);
- }
-
- public Long asMsPeriod() {
- if (unit.equals(Unit.s))
- return Math.round(1000d / value);
- else if (unit.equals(Unit.m))
- return Math.round((60d * 1000d) / value);
- else if (unit.equals(Unit.h))
- return Math.round((60d * 60d * 1000d) / value);
- else if (unit.equals(Unit.d))
- return Math.round((24d * 60d * 60d * 1000d) / value);
- else
- throw new IllegalArgumentException("Unsupported unit " + unit);
- }
-
- @Override
- public String toString() {
- return usNumberFormat.format(value) + '/' + unit;
- }
-
- public Double getValue() {
- return value;
- }
-
- public Unit getUnit() {
- return unit;
- }
-
-}
+++ /dev/null
-package org.argeo.util;
-
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.security.SecureRandom;
-import java.time.Duration;
-import java.time.LocalDateTime;
-import java.time.ZoneOffset;
-import java.util.BitSet;
-import java.util.Random;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Utilities to simplify and extends usage of {@link UUID}. Only the RFC 4122
- * variant (also known as Leach–Salz variant) is supported.
- */
-public class UuidUtils {
- /** Nil UUID (00000000-0000-0000-0000-000000000000). */
- public final static UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000");
- public final static LocalDateTime GREGORIAN_START = LocalDateTime.of(1582, 10, 15, 0, 0, 0);
-
- private final static long MOST_SIG_VERSION1 = (1l << 12);
- private final static long LEAST_SIG_RFC4122_VARIANT = (1l << 63);
-
- private final static SecureRandom RANDOM;
- private final static AtomicInteger CLOCK_SEQUENCE;
- private final static byte[] HARDWARE_ADDRESS;
- /** A start timestamp to which {@link System#nanoTime()}/100 can be added. */
- private final static long START_TIMESTAMP;
- static {
- RANDOM = new SecureRandom();
- CLOCK_SEQUENCE = new AtomicInteger(RANDOM.nextInt(16384));
- HARDWARE_ADDRESS = getHardwareAddress();
-
- long nowVm = System.nanoTime() / 100;
- Duration duration = Duration.between(GREGORIAN_START, LocalDateTime.now(ZoneOffset.UTC));
- START_TIMESTAMP = (duration.getSeconds() * 10000000 + duration.getNano() / 100) - nowVm;
- }
-
- private static byte[] getHardwareAddress() {
- InetAddress localHost;
- try {
- localHost = InetAddress.getLocalHost();
- try {
- NetworkInterface nic = NetworkInterface.getByInetAddress(localHost);
- return nic.getHardwareAddress();
- } catch (SocketException e) {
- return null;
- }
- } catch (UnknownHostException e) {
- return null;
- }
-
- }
-
- public static UUID timeUUIDwithRandomNode() {
- long timestamp = START_TIMESTAMP + System.nanoTime() / 100;
- return timeUUID(timestamp, RANDOM);
- }
-
- public static UUID timeUUID(long timestamp, Random random) {
- byte[] node = new byte[6];
- random.nextBytes(node);
- node[0] = (byte) (node[0] | 1);
- long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
- return timeUUID(timestamp, clockSequence, node);
- }
-
- public static UUID timeUUID() {
- long timestamp = START_TIMESTAMP + System.nanoTime() / 100;
- return timeUUID(timestamp);
- }
-
- public static UUID timeUUID(long timestamp) {
- if (HARDWARE_ADDRESS == null)
- return timeUUID(timestamp, RANDOM);
- long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
- return timeUUID(timestamp, clockSequence, HARDWARE_ADDRESS);
- }
-
- public static UUID timeUUID(long timestamp, NetworkInterface nic) {
- byte[] node;
- try {
- node = nic.getHardwareAddress();
- } catch (SocketException e) {
- throw new IllegalStateException("Cannot get hardware address", e);
- }
- long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
- return timeUUID(timestamp, clockSequence, node);
- }
-
- public static UUID timeUUID(LocalDateTime time, long clockSequence, byte[] node) {
- Duration duration = Duration.between(GREGORIAN_START, time);
- // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000
- long timestamp = duration.getSeconds() * 10000000 + duration.getNano() / 100;
- return timeUUID(timestamp, clockSequence, node);
- }
-
- public static UUID timeUUID(long timestamp, long clockSequence, byte[] node) {
- assert node.length >= 6;
-
- long mostSig = MOST_SIG_VERSION1 // base for version 1 UUID
- | ((timestamp & 0xFFFFFFFFL) << 32) // time_low
- | (((timestamp >> 32) & 0xFFFFL) << 16) // time_mid
- | ((timestamp >> 48) & 0x0FFFL);// time_hi_and_version
-
- long leastSig = LEAST_SIG_RFC4122_VARIANT // base for Leach–Salz UUID
- | (((clockSequence & 0x3F00) >> 8) << 56) // clk_seq_hi_res
- | ((clockSequence & 0xFF) << 48) // clk_seq_low
- | (node[0] & 0xFFL) //
- | ((node[1] & 0xFFL) << 8) //
- | ((node[2] & 0xFFL) << 16) //
- | ((node[3] & 0xFFL) << 24) //
- | ((node[4] & 0xFFL) << 32) //
- | ((node[5] & 0xFFL) << 40); //
-// for (int i = 0; i < 6; i++) {
-// leastSig = leastSig | ((node[i] & 0xFFL) << (8 * i));
-// }
- UUID uuid = new UUID(mostSig, leastSig);
-
- // tests
- assert uuid.node() == BitSet.valueOf(node).toLongArray()[0];
- assert uuid.timestamp() == timestamp;
- assert uuid.clockSequence() == clockSequence;
- assert uuid.version() == 1;
- assert uuid.variant() == 2;
- return uuid;
- }
-
- @Deprecated
- public static UUID timeBasedUUID() {
- return timeBasedUUID(LocalDateTime.now(ZoneOffset.UTC));
- }
-
- @Deprecated
- public static UUID timeBasedRandomUUID() {
- return timeBasedRandomUUID(LocalDateTime.now(ZoneOffset.UTC), RANDOM);
- }
-
- @Deprecated
- public static UUID timeBasedUUID(LocalDateTime time) {
- if (HARDWARE_ADDRESS == null)
- return timeBasedRandomUUID(time, RANDOM);
- return timeBasedUUID(time, BitSet.valueOf(HARDWARE_ADDRESS));
- }
-
- @Deprecated
- public static UUID timeBasedAddressUUID(LocalDateTime time, NetworkInterface nic) throws SocketException {
- byte[] nodeBytes = nic.getHardwareAddress();
- BitSet node = BitSet.valueOf(nodeBytes);
- return timeBasedUUID(time, node);
- }
-
- @Deprecated
- public static UUID timeBasedRandomUUID(LocalDateTime time, Random random) {
- byte[] nodeBytes = new byte[6];
- random.nextBytes(nodeBytes);
- BitSet node = BitSet.valueOf(nodeBytes);
- // set random marker
- node.set(0, true);
- return timeBasedUUID(time, node);
- }
-
- @Deprecated
- public static UUID timeBasedUUID(LocalDateTime time, BitSet node) {
- // most significant
- Duration duration = Duration.between(GREGORIAN_START, time);
-
- // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000
- long timeNanos = duration.getSeconds() * 10000000 + duration.getNano() / 100;
- BitSet timeBits = BitSet.valueOf(new long[] { timeNanos });
- assert timeBits.length() <= 60;
-
- int clockSequence;
- synchronized (CLOCK_SEQUENCE) {
- clockSequence = CLOCK_SEQUENCE.incrementAndGet();
- if (clockSequence > 16384)
- CLOCK_SEQUENCE.set(0);
- }
- BitSet clockSequenceBits = BitSet.valueOf(new long[] { clockSequence });
-
- // Build the UUID, bit by bit
- // see https://tools.ietf.org/html/rfc4122#section-4.2.2
- // time
- BitSet time_low = new BitSet(32);
- BitSet time_mid = new BitSet(16);
- BitSet time_hi_and_version = new BitSet(16);
-
- for (int i = 0; i < 60; i++) {
- if (i < 32)
- time_low.set(i, timeBits.get(i));
- else if (i < 48)
- time_mid.set(i - 32, timeBits.get(i));
- else
- time_hi_and_version.set(i - 48, timeBits.get(i));
- }
- // version
- time_hi_and_version.set(12, true);
- time_hi_and_version.set(13, false);
- time_hi_and_version.set(14, false);
- time_hi_and_version.set(15, false);
-
- // clock sequence
- BitSet clk_seq_hi_res = new BitSet(8);
- BitSet clk_seq_low = new BitSet(8);
- for (int i = 0; i < 8; i++) {
- clk_seq_low.set(i, clockSequenceBits.get(i));
- }
- for (int i = 8; i < 14; i++) {
- clk_seq_hi_res.set(i - 8, clockSequenceBits.get(i));
- }
- // variant
- clk_seq_hi_res.set(6, false);
- clk_seq_hi_res.set(7, true);
-
-// String str = toHexString(time_low.toLongArray()[0]) + "-" + toHexString(time_mid.toLongArray()[0]) + "-"
-// + toHexString(time_hi_and_version.toLongArray()[0]) + "-"
-// + toHexString(clock_seq_hi_and_reserved.toLongArray()[0]) + toHexString(clock_seq_low.toLongArray()[0])
-// + "-" + toHexString(node.toLongArray()[0]);
-// UUID uuid = UUID.fromString(str);
-
- BitSet uuidBits = new BitSet(128);
- for (int i = 0; i < 128; i++) {
- if (i < 48)
- uuidBits.set(i, node.get(i));
- else if (i < 56)
- uuidBits.set(i, clk_seq_low.get(i - 48));
- else if (i < 64)
- uuidBits.set(i, clk_seq_hi_res.get(i - 56));
- else if (i < 80)
- uuidBits.set(i, time_hi_and_version.get(i - 64));
- else if (i < 96)
- uuidBits.set(i, time_mid.get(i - 80));
- else
- uuidBits.set(i, time_low.get(i - 96));
- }
-
- long[] uuidLongs = uuidBits.toLongArray();
- assert uuidLongs.length == 2;
- UUID uuid = new UUID(uuidLongs[1], uuidLongs[0]);
-
- // tests
- assert uuid.node() == node.toLongArray()[0];
- assert uuid.timestamp() == timeNanos;
- assert uuid.clockSequence() == clockSequence;
- assert uuid.version() == 1;
- assert uuid.variant() == 2;
- return uuid;
- }
-
- public static String toBinaryString(UUID uuid, int charsPerSegment, char separator) {
- String binaryString = toBinaryString(uuid);
- StringBuilder sb = new StringBuilder(128 + (128 / charsPerSegment));
- for (int i = 0; i < binaryString.length(); i++) {
- if (i != 0 && i % charsPerSegment == 0)
- sb.append(separator);
- sb.append(binaryString.charAt(i));
- }
- return sb.toString();
- }
-
- public static String toBinaryString(UUID uuid) {
- String most = zeroTo64Chars(Long.toBinaryString(uuid.getMostSignificantBits()));
- String least = zeroTo64Chars(Long.toBinaryString(uuid.getLeastSignificantBits()));
- String binaryString = most + least;
- assert binaryString.length() == 128;
- return binaryString;
- }
-
- private static String zeroTo64Chars(String str) {
- assert str.length() <= 64;
- if (str.length() < 64) {
- StringBuilder sb = new StringBuilder(64);
- for (int i = 0; i < 64 - str.length(); i++)
- sb.append('0');
- sb.append(str);
- return sb.toString();
- } else
- return str;
- }
-
- public static String compactToStd(String compact) {
- if (compact.length() != 32)
- throw new IllegalArgumentException(
- "Compact UUID '" + compact + "' has length " + compact.length() + " and not 32.");
- StringBuilder sb = new StringBuilder(36);
- for (int i = 0; i < 32; i++) {
- if (i == 8 || i == 12 || i == 16 || i == 20)
- sb.append('-');
- sb.append(compact.charAt(i));
- }
- String std = sb.toString();
- assert std.length() == 36;
- assert UUID.fromString(std).toString().equals(std);
- return std;
- }
-
- public static UUID compactToUuid(String compact) {
- return UUID.fromString(compactToStd(compact));
- }
-
- public static String firstBlock(UUID uuid) {
- return uuid.toString().substring(0, 8);
- }
-
- public static boolean isRandom(UUID uuid) {
- return uuid.version() == 4;
- }
-
- public static boolean isTimeBased(UUID uuid) {
- return uuid.version() == 1;
- }
-
- public static boolean isTimeBasedRandom(UUID uuid) {
- if (uuid.version() == 1) {
- BitSet node = BitSet.valueOf(new long[] { uuid.node() });
- return node.get(0);
- } else
- return false;
- }
-
- public static boolean isNameBased(UUID uuid) {
- return uuid.version() == 3 || uuid.version() == 5;
- }
-
- /** Singleton. */
- private UuidUtils() {
- }
-
- public final static void main(String[] args) throws Exception {
- UUID uuid;
-
-// uuid = compactToUuid("996b1f5122de4b2f94e49168d32f22d1");
-// System.out.println(uuid.toString() + ", isRandom=" + isRandom(uuid));
-
- // warm up before measuring perf
- for (int i = 0; i < 10; i++) {
- UUID.randomUUID();
- timeUUID();
- timeUUIDwithRandomNode();
- timeBasedRandomUUID();
- timeBasedUUID();
- }
-
- long begin;
- long duration;
-
- begin = System.nanoTime();
- uuid = UUID.randomUUID();
- duration = System.nanoTime() - begin;
- System.out.println(uuid.toString() + " in " + duration + " ns, isRandom=" + isRandom(uuid));
-
- begin = System.nanoTime();
- uuid = timeUUID();
- duration = System.nanoTime() - begin;
- System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
-
- begin = System.nanoTime();
- uuid = timeUUIDwithRandomNode();
- duration = System.nanoTime() - begin;
- System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
-
- begin = System.nanoTime();
- uuid = timeBasedUUID();
- duration = System.nanoTime() - begin;
- System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
-
- begin = System.nanoTime();
- uuid = timeBasedRandomUUID();
- duration = System.nanoTime() - begin;
- System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
-// System.out.println(toBinaryString(uuid, 8, ' '));
-// System.out.println(toBinaryString(uuid, 16, '\n'));
- }
-}
+++ /dev/null
-/** Generic Java utilities. */
-package org.argeo.util;
\ No newline at end of file
+++ /dev/null
-package org.argeo.util.register;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.function.Consumer;
-
-/**
- * A wrapper for an object, whose dependencies and life cycle can be managed.
- */
-public class Component<I> {
-
- private final I instance;
-
- private final Runnable init;
- private final Runnable close;
-
- private final Map<Class<? super I>, PublishedType<? super I>> types;
- private final Set<Dependency<?>> dependencies;
-
- private CompletableFuture<Void> activationStarted = null;
- private CompletableFuture<Void> activated = null;
-
- private CompletableFuture<Void> deactivationStarted = null;
- private CompletableFuture<Void> deactivated = null;
-
- private Set<Dependency<?>> dependants = new HashSet<>();
-
- Component(Consumer<Component<?>> register, I instance, Runnable init, Runnable close,
- Set<Dependency<?>> dependencies, Set<Class<? super I>> classes) {
- assert instance != null;
- assert init != null;
- assert close != null;
- assert dependencies != null;
- assert classes != null;
-
- this.instance = instance;
- this.init = init;
- this.close = close;
-
- // types
- Map<Class<? super I>, PublishedType<? super I>> types = new HashMap<>(classes.size());
- for (Class<? super I> clss : classes) {
-// if (!clss.isAssignableFrom(instance.getClass()))
-// throw new IllegalArgumentException(
-// "Type " + clss.getName() + " is not compatible with " + instance.getClass().getName());
- types.put(clss, new PublishedType<>(this, clss));
- }
- this.types = Collections.unmodifiableMap(types);
-
- // dependencies
- this.dependencies = Collections.unmodifiableSet(new HashSet<>(dependencies));
- for (Dependency<?> dependency : this.dependencies) {
- dependency.setDependantComponent(this);
- }
-
- // deactivated by default
- deactivated = CompletableFuture.completedFuture(null);
- deactivationStarted = CompletableFuture.completedFuture(null);
-
- // TODO check whether context is active, so that we start right away
- prepareNextActivation();
-
- register.accept(this);
- }
-
- private void prepareNextActivation() {
- activationStarted = new CompletableFuture<Void>();
- activated = activationStarted //
- .thenComposeAsync(this::dependenciesActivated) //
- .thenRun(this.init) //
- .thenRun(() -> prepareNextDeactivation());
- }
-
- private void prepareNextDeactivation() {
- deactivationStarted = new CompletableFuture<Void>();
- deactivated = deactivationStarted //
- .thenComposeAsync(this::dependantsDeactivated) //
- .thenRun(this.close) //
- .thenRun(() -> prepareNextActivation());
- }
-
- public CompletableFuture<Void> getActivated() {
- return activated;
- }
-
- public CompletableFuture<Void> getDeactivated() {
- return deactivated;
- }
-
- void startActivating() {
- if (activated.isDone() || activationStarted.isDone())
- return;
- activationStarted.complete(null);
- }
-
- void startDeactivating() {
- if (deactivated.isDone() || deactivationStarted.isDone())
- return;
- deactivationStarted.complete(null);
- }
-
- CompletableFuture<Void> dependenciesActivated(Void v) {
- Set<CompletableFuture<?>> constraints = new HashSet<>(this.dependencies.size());
- for (Dependency<?> dependency : this.dependencies) {
- CompletableFuture<Void> dependencyActivated = dependency.publisherActivated() //
- .thenCompose(dependency::set);
- constraints.add(dependencyActivated);
- }
- return CompletableFuture.allOf(constraints.toArray(new CompletableFuture[constraints.size()]));
- }
-
- CompletableFuture<Void> dependantsDeactivated(Void v) {
- Set<CompletableFuture<?>> constraints = new HashSet<>(this.dependants.size());
- for (Dependency<?> dependant : this.dependants) {
- CompletableFuture<Void> dependantDeactivated = dependant.dependantDeactivated() //
- .thenCompose(dependant::unset);
- constraints.add(dependantDeactivated);
- }
- CompletableFuture<Void> dependantsDeactivated = CompletableFuture
- .allOf(constraints.toArray(new CompletableFuture[constraints.size()]));
- return dependantsDeactivated;
-
- }
-
- void addDependant(Dependency<?> dependant) {
- dependants.add(dependant);
- }
-
- I getInstance() {
- return instance;
- }
-
- @SuppressWarnings("unchecked")
- <T> PublishedType<T> getType(Class<T> clss) {
- if (!types.containsKey(clss))
- throw new IllegalArgumentException(clss.getName() + " is not a type published by this component");
- return (PublishedType<T>) types.get(clss);
- }
-
- <T> boolean isPublishedType(Class<T> clss) {
- return types.containsKey(clss);
- }
-
- public static class PublishedType<T> {
- private Component<? extends T> component;
- private Class<T> clss;
-
-// private CompletableFuture<Component<? extends T>> publisherAvailable;
- private CompletableFuture<T> value;
-
- public PublishedType(Component<? extends T> component, Class<T> clss) {
- this.clss = clss;
- this.component = component;
- value = CompletableFuture.completedFuture((T) component.instance);
-// value = publisherAvailable.thenApply((c) -> c.getInstance());
- }
-
- Component<?> getPublisher() {
- return component;
- }
-
-// CompletableFuture<Component<? extends T>> publisherAvailable() {
-// return publisherAvailable;
-// }
-
- Class<T> getType() {
- return clss;
- }
- }
-
- public static class Builder<I> {
- private final I instance;
-
- private Runnable init;
- private Runnable close;
-
- private Set<Dependency<?>> dependencies = new HashSet<>();
- private Set<Class<? super I>> types = new HashSet<>();
-
- public Builder(I instance) {
- this.instance = instance;
- }
-
- public Component<I> build(Consumer<Component<?>> register) {
- // default values
- if (types.isEmpty()) {
- types.add(getInstanceClass());
- }
-
- if (init == null)
- init = () -> {
- };
- if (close == null)
- close = () -> {
- };
-
- // instantiation
- Component<I> component = new Component<I>(register, instance, init, close, dependencies, types);
- for (Dependency<?> dependency : dependencies) {
- dependency.type.getPublisher().addDependant(dependency);
- }
- return component;
- }
-
- public Builder<I> addType(Class<? super I> clss) {
- types.add(clss);
- return this;
- }
-
- public Builder<I> addInit(Runnable init) {
- if (this.init != null)
- throw new IllegalArgumentException("init method is already set");
- this.init = init;
- return this;
- }
-
- public Builder<I> addClose(Runnable close) {
- if (this.close != null)
- throw new IllegalArgumentException("close method is already set");
- this.close = close;
- return this;
- }
-
- public <D> Builder<I> addDependency(PublishedType<D> type, Consumer<D> set, Consumer<D> unset) {
- dependencies.add(new Dependency<D>(type, set, unset));
- return this;
- }
-
- public I get() {
- return instance;
- }
-
- @SuppressWarnings("unchecked")
- private Class<I> getInstanceClass() {
- return (Class<I>) instance.getClass();
- }
-
- }
-
- static class Dependency<D> {
- private PublishedType<D> type;
- private Consumer<D> set;
- private Consumer<D> unset;
-
- // live
- Component<?> dependantComponent;
- CompletableFuture<Void> setStage;
- CompletableFuture<Void> unsetStage;
-
- public Dependency(PublishedType<D> types, Consumer<D> set, Consumer<D> unset) {
- super();
- this.type = types;
- this.set = set;
- this.unset = unset != null ? unset : (v) -> set.accept(null);
- }
-
- // live
- void setDependantComponent(Component<?> component) {
- this.dependantComponent = component;
- }
-
- CompletableFuture<Void> publisherActivated() {
- return type.getPublisher().activated.copy();
- }
-
- CompletableFuture<Void> dependantDeactivated() {
- return dependantComponent.deactivated.copy();
- }
-
- CompletableFuture<Void> set(Void v) {
- return type.value.thenAccept(set);
- }
-
- CompletableFuture<Void> unset(Void v) {
- return type.value.thenAccept(unset);
- }
-
- }
-}
+++ /dev/null
-package org.argeo.util.register;
-
-import java.util.Map;
-
-/** A dynamic register of objects. */
-public interface Register {
- <T> Singleton<T> set(T obj, Class<T> clss, Map<String, Object> attributes, Class<?>... classes);
-
- void shutdown();
-}
+++ /dev/null
-package org.argeo.util.register;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.concurrent.Future;
-import java.util.function.Consumer;
-
-public class Singleton<T> {
- private final Class<T> clss;
- private final CompletableFuture<T> registrationStage;
- private final List<Consumer<T>> unregistrationHooks = new ArrayList<>();
-
- public Singleton(Class<T> clss, CompletableFuture<T> registrationStage) {
- this.clss = clss;
- this.registrationStage = registrationStage;
- }
-
- CompletionStage<T> getRegistrationStage() {
- return registrationStage.minimalCompletionStage();
- }
-
- public void addUnregistrationHook(Consumer<T> todo) {
- unregistrationHooks.add(todo);
- }
-
- public Future<T> getValue() {
- return registrationStage.copy();
- }
-
- public CompletableFuture<Void> prepareUnregistration(Void v) {
- List<CompletableFuture<Void>> lst = new ArrayList<>();
- for (Consumer<T> hook : unregistrationHooks) {
- lst.add(registrationStage.thenAcceptAsync(hook));
- }
- CompletableFuture<Void> prepareUnregistrationStage = CompletableFuture
- .allOf(lst.toArray(new CompletableFuture<?>[lst.size()]));
- return prepareUnregistrationStage;
- }
-}
+++ /dev/null
-package org.argeo.util.register;
-
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-/** A minimal component register. */
-public class StaticRegister {
- private final static AtomicBoolean started = new AtomicBoolean(false);
- private final static IdentityHashMap<Object, Component<?>> components = new IdentityHashMap<>();
-
- public static Consumer<Component<?>> asConsumer() {
- return (c) -> registerComponent(c);
- }
-
-// public static BiFunction<Class<?>, Predicate<Map<String, Object>>, Component<?>> asProvider() {
-//
-// }
-
- static synchronized <T> Component<? extends T> find(Class<T> clss, Predicate<Map<String, Object>> filter) {
- Set<Component<? extends T>> result = new HashSet<>();
- instances: for (Object instance : components.keySet()) {
- if (!clss.isAssignableFrom(instance.getClass()))
- continue instances;
- Component<? extends T> component = (Component<? extends T>) components.get(instance);
-
- // TODO filter
- if (component.isPublishedType(clss))
- result.add(component);
- }
- if (result.isEmpty())
- return null;
- return result.iterator().next();
-
- }
-
- static synchronized void registerComponent(Component<?> component) {
- if (started.get()) // TODO make it really dynamic
- throw new IllegalStateException("Already activated");
- if (components.containsKey(component.getInstance()))
- throw new IllegalArgumentException("Already registered as component");
- components.put(component.getInstance(), component);
- }
-
- static synchronized Component<?> get(Object instance) {
- if (!components.containsKey(instance))
- throw new IllegalArgumentException("Not registered as component");
- return components.get(instance);
- }
-
- synchronized static void activate() {
- if (started.get())
- throw new IllegalStateException("Already activated");
- Set<CompletableFuture<?>> constraints = new HashSet<>();
- for (Component<?> component : components.values()) {
- component.startActivating();
- constraints.add(component.getActivated());
- }
-
- // wait
- try {
- CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(true))
- .get();
- } catch (ExecutionException | InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
-
- synchronized static void deactivate() {
- if (!started.get())
- throw new IllegalStateException("Not activated");
- Set<CompletableFuture<?>> constraints = new HashSet<>();
- for (Component<?> component : components.values()) {
- component.startDeactivating();
- constraints.add(component.getDeactivated());
- }
-
- // wait
- try {
- CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(false))
- .get();
- } catch (ExecutionException | InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
-
- synchronized static void clear() {
- components.clear();
- }
-
-}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
+ <attributes>
+ <attribute name="module" value="true"/>
+ </attributes>
+ </classpathentry>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.argeo.util</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ </natures>
+</projectDescription>
--- /dev/null
+/MANIFEST.MF
--- /dev/null
+Bundle-Activator: org.argeo.osgi.internal.EnterpriseActivator
+Bundle-ActivationPolicy: lazy
+
+Import-Package: org.osgi.*;version=0.0.0,\
+!org.apache.commons.logging,\
+*
--- /dev/null
+source.. = src/
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<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.argeo.commons</groupId>
+ <artifactId>argeo-commons</artifactId>
+ <version>2.3-SNAPSHOT</version>
+ <relativePath>..</relativePath>
+ </parent>
+ <artifactId>org.argeo.util</artifactId>
+ <name>Java and OSGi utilities</name>
+</project>
\ No newline at end of file
--- /dev/null
+package org.argeo.ident;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.ConnectException;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * A simple ident client, supporting authd OpenSSL encrypted username.
+ *
+ * @see RFC 1413 https://tools.ietf.org/html/rfc1413
+ */
+public class IdentClient {
+ public final static int DEFAULT_IDENT_PORT = 113;
+ public final static String AUTHD_PASSPHRASE_PATH = "/etc/ident.key";
+ final static String NO_USER = "NO-USER";
+
+ private final String host;
+ private final int port;
+
+ private OpenSslDecryptor openSslDecryptor = new OpenSslDecryptor();
+ private String identPassphrase = null;
+
+ public IdentClient(String host) {
+ this(host, readPassphrase(AUTHD_PASSPHRASE_PATH), DEFAULT_IDENT_PORT);
+ }
+
+ public IdentClient(String host, Path passPhrasePath) {
+ this(host, readPassphrase(passPhrasePath), DEFAULT_IDENT_PORT);
+ }
+
+ public IdentClient(String host, String identPassphrase) {
+ this(host, identPassphrase, DEFAULT_IDENT_PORT);
+ }
+
+ public IdentClient(String host, String identPassphrase, int port) {
+ this.host = host;
+ this.identPassphrase = identPassphrase;
+ this.port = port;
+ }
+
+ /** @return the username or null if none */
+ public String getUsername(int serverPort, int clientPort) {
+ String answer;
+ try (Socket socket = new Socket(host, port)) {
+ String msg = clientPort + "," + serverPort + "\n";
+ OutputStream out = socket.getOutputStream();
+ out.write(msg.getBytes(StandardCharsets.US_ASCII));
+ out.flush();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ answer = reader.readLine();
+ } catch (ConnectException e) {
+ System.err.println(
+ "Ident client is configured but no ident server available on " + host + " (port " + port + ")");
+ return null;
+ } catch (Exception e) {
+ throw new RuntimeException("Cannot read from ident server on " + host + " (port " + port + ")", e);
+ }
+ StringTokenizer st = new StringTokenizer(answer, " :\n");
+ String username = null;
+ while (st.hasMoreTokens())
+ username = st.nextToken();
+
+ if (username.equals(NO_USER))
+ return null;
+
+ if (identPassphrase != null && username.startsWith("[")) {
+ String encrypted = username.substring(1, username.length() - 1);
+ username = openSslDecryptor.decryptAuthd(encrypted, identPassphrase).trim();
+ }
+// System.out.println(username);
+ return username;
+ }
+
+ public void setOpenSslDecryptor(OpenSslDecryptor openSslDecryptor) {
+ this.openSslDecryptor = openSslDecryptor;
+ }
+
+ public static String readPassphrase(String filePath) {
+ return readPassphrase(Paths.get(filePath));
+ }
+
+ /** @return the first line of the file. */
+ public static String readPassphrase(Path path) {
+ if (!isPathAvailable(path))
+ return null;
+ List<String> lines;
+ try {
+ lines = Files.readAllLines(path);
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot read " + path, e);
+ }
+ if (lines.size() == 0)
+ return null;
+ String passphrase = lines.get(0);
+ return passphrase;
+ }
+
+ public static boolean isDefaultAuthdPassphraseFileAvailable() {
+ return isPathAvailable(Paths.get(AUTHD_PASSPHRASE_PATH));
+ }
+
+ public static boolean isPathAvailable(Path path) {
+ if (!Files.exists(path))
+ return false;
+ if (!Files.isReadable(path))
+ return false;
+ return true;
+ }
+
+ public static void main(String[] args) {
+ IdentClient identClient = new IdentClient("127.0.0.1", "changeit");
+ String username = identClient.getUsername(7070, 55958);
+ System.out.println(username);
+ }
+}
--- /dev/null
+package org.argeo.ident;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.Base64;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Decrypts OpenSSL encrypted data.
+ *
+ * From
+ * https://stackoverflow.com/questions/11783062/how-to-decrypt-file-in-java-encrypted-with-openssl-command-using-aes
+ *
+ * See also
+ * https://stackoverflow.com/questions/54171959/badpadding-exception-when-trying-to-decrypt-aes-based-encrypted-text/54173509#54173509
+ * for new default message digest (not yet in CentOS 7 as of July 2019)
+ */
+public class OpenSslDecryptor {
+ private static final int INDEX_KEY = 0;
+ private static final int INDEX_IV = 1;
+ private static final int ITERATIONS = 1;
+
+ private static final int SALT_OFFSET = 8;
+ private static final int SALT_SIZE = 8;
+ private static final int CIPHERTEXT_OFFSET = SALT_OFFSET + SALT_SIZE;
+
+ /** In bits. */
+ private final int keySize;
+
+ private Cipher cipher;
+ private MessageDigest messageDigest;
+
+ public OpenSslDecryptor() {
+ /*
+ * Changed to SHA-256 from OpenSSL v1.1.0 (see
+ * https://stackoverflow.com/questions/39637388/encryption-decryption-doesnt-
+ * work-well-between-two-different-openssl-versions)
+ */
+ this(128, "MD5");
+ }
+
+ public OpenSslDecryptor(int keySize, String messageDigest) {
+ this.keySize = keySize;
+ try {
+ this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ this.messageDigest = MessageDigest.getInstance(messageDigest);
+ } catch (GeneralSecurityException e) {
+ throw new IllegalArgumentException("Cannot initialise decryptor", e);
+ }
+ }
+
+ public String decryptAuthd(String dataBase64, String passphrase) {
+ try {
+ byte[] headerSaltAndCipherText = Base64.getDecoder().decode(dataBase64);
+
+ boolean withSalt = true;
+ byte[] salt = withSalt ? Arrays.copyOfRange(headerSaltAndCipherText, SALT_OFFSET, SALT_OFFSET + SALT_SIZE)
+ : null;
+ byte[] encrypted = withSalt
+ ? Arrays.copyOfRange(headerSaltAndCipherText, CIPHERTEXT_OFFSET, headerSaltAndCipherText.length)
+ : headerSaltAndCipherText;
+
+ final byte[][] keyAndIV = EVP_BytesToKey(keySize / Byte.SIZE, cipher.getBlockSize(), messageDigest, salt,
+ passphrase.getBytes(StandardCharsets.US_ASCII), ITERATIONS);
+ SecretKeySpec key = new SecretKeySpec(keyAndIV[INDEX_KEY], "AES");
+ IvParameterSpec iv = new IvParameterSpec(keyAndIV[INDEX_IV]);
+
+ cipher.init(Cipher.DECRYPT_MODE, key, iv);
+ byte[] decrypted = cipher.doFinal(encrypted);
+
+ String answer = new String(decrypted, StandardCharsets.US_ASCII);
+ return answer;
+ } catch (BadPaddingException e) {
+ throw new IllegalStateException("Bad password, algorithm, mode or padding;"
+ + " no salt, wrong number of iterations or corrupted ciphertext.", e);
+ } catch (IllegalBlockSizeException e) {
+ throw new IllegalStateException("Bad algorithm, mode or corrupted (resized) ciphertext.", e);
+ } catch (GeneralSecurityException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static byte[][] EVP_BytesToKey(int key_len, int iv_len, MessageDigest md, byte[] salt, byte[] data,
+ int count) {
+ byte[][] both = new byte[2][];
+ byte[] key = new byte[key_len];
+ int key_ix = 0;
+ byte[] iv = new byte[iv_len];
+ int iv_ix = 0;
+ both[0] = key;
+ both[1] = iv;
+ byte[] md_buf = null;
+ int nkey = key_len;
+ int niv = iv_len;
+ int i = 0;
+ if (data == null) {
+ return both;
+ }
+ int addmd = 0;
+ for (;;) {
+ md.reset();
+ if (addmd++ > 0) {
+ md.update(md_buf);
+ }
+ md.update(data);
+ if (null != salt) {
+ md.update(salt, 0, 8);
+ }
+ md_buf = md.digest();
+ for (i = 1; i < count; i++) {
+ md.reset();
+ md.update(md_buf);
+ md_buf = md.digest();
+ }
+ i = 0;
+ if (nkey > 0) {
+ for (;;) {
+ if (nkey == 0)
+ break;
+ if (i == md_buf.length)
+ break;
+ key[key_ix++] = md_buf[i];
+ nkey--;
+ i++;
+ }
+ }
+ if (niv > 0 && i != md_buf.length) {
+ for (;;) {
+ if (niv == 0)
+ break;
+ if (i == md_buf.length)
+ break;
+ iv[iv_ix++] = md_buf[i];
+ niv--;
+ i++;
+ }
+ }
+ if (nkey == 0 && niv == 0) {
+ break;
+ }
+ }
+ for (i = 0; i < md_buf.length; i++) {
+ md_buf[i] = 0;
+ }
+ return both;
+ }
+
+ public static void main(String[] args) {
+ String dataBase64 = args[0];
+ String passphrase = args[1];
+ OpenSslDecryptor decryptor = new OpenSslDecryptor();
+ System.out.println(decryptor.decryptAuthd(dataBase64, passphrase));
+ }
+
+}
\ No newline at end of file
--- /dev/null
+/** Ident authentication protocol support. */
+package org.argeo.ident;
\ No newline at end of file
--- /dev/null
+package org.argeo.naming;
+
+import java.util.Dictionary;
+import java.util.Enumeration;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+
+public class AttributesDictionary extends Dictionary<String, Object> {
+ private final Attributes attributes;
+
+ /** The provided attributes is wrapped, not copied. */
+ public AttributesDictionary(Attributes attributes) {
+ if (attributes == null)
+ throw new IllegalArgumentException("Attributes cannot be null");
+ this.attributes = attributes;
+ }
+
+ @Override
+ public int size() {
+ return attributes.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return attributes.size() == 0;
+ }
+
+ @Override
+ public Enumeration<String> keys() {
+ NamingEnumeration<String> namingEnumeration = attributes.getIDs();
+ return new Enumeration<String>() {
+
+ @Override
+ public boolean hasMoreElements() {
+ return namingEnumeration.hasMoreElements();
+ }
+
+ @Override
+ public String nextElement() {
+ return namingEnumeration.nextElement();
+ }
+
+ };
+ }
+
+ @Override
+ public Enumeration<Object> elements() {
+ NamingEnumeration<String> namingEnumeration = attributes.getIDs();
+ return new Enumeration<Object>() {
+
+ @Override
+ public boolean hasMoreElements() {
+ return namingEnumeration.hasMoreElements();
+ }
+
+ @Override
+ public Object nextElement() {
+ String key = namingEnumeration.nextElement();
+ return get(key);
+ }
+
+ };
+ }
+
+ @Override
+ /** @returns a <code>String</code> or <code>String[]</code> */
+ public Object get(Object key) {
+ try {
+ if (key == null)
+ throw new IllegalArgumentException("Key cannot be null");
+ Attribute attr = attributes.get(key.toString());
+ if (attr == null)
+ return null;
+ if (attr.size() == 0)
+ throw new IllegalStateException("There must be at least one value");
+ else if (attr.size() == 1) {
+ return attr.get().toString();
+ } else {// multiple
+ String[] res = new String[attr.size()];
+ for (int i = 0; i < attr.size(); i++) {
+ Object value = attr.get();
+ if (value == null)
+ throw new RuntimeException("Values cannot be null");
+ res[i] = attr.get(i).toString();
+ }
+ return res;
+ }
+ } catch (NamingException e) {
+ throw new RuntimeException("Cannot get value for " + key, e);
+ }
+ }
+
+ @Override
+ public Object put(String key, Object value) {
+ if (key == null)
+ throw new IllegalArgumentException("Key cannot be null");
+ if (value == null)
+ throw new IllegalArgumentException("Value cannot be null");
+
+ Object oldValue = get(key);
+ Attribute attr = attributes.get(key);
+ if (attr == null) {
+ attr = new BasicAttribute(key);
+ attributes.put(attr);
+ }
+
+ if (value instanceof String[]) {
+ String[] values = (String[]) value;
+ // clean additional values
+ for (int i = values.length; i < attr.size(); i++)
+ attr.remove(i);
+ // set values
+ for (int i = 0; i < values.length; i++) {
+ attr.set(i, values[i]);
+ }
+ } else {
+ if (attr.size() > 1)
+ throw new IllegalArgumentException("Attribute " + key + " is multi-valued");
+ if (attr.size() == 1) {
+ try {
+ if (!attr.get(0).equals(value))
+ attr.set(0, value.toString());
+ } catch (NamingException e) {
+ throw new RuntimeException("Cannot check existing value", e);
+ }
+ } else {
+ attr.add(value.toString());
+ }
+ }
+ return oldValue;
+ }
+
+ @Override
+ public Object remove(Object key) {
+ if (key == null)
+ throw new IllegalArgumentException("Key cannot be null");
+ Object oldValue = get(key);
+ if (oldValue == null)
+ return null;
+ return attributes.remove(key.toString());
+ }
+
+ /**
+ * Copy the <b>content</b> of an {@link Attributes} to the provided
+ * {@link Dictionary}.
+ */
+ public static void copy(Attributes attributes, Dictionary<String, Object> dictionary) {
+ AttributesDictionary ad = new AttributesDictionary(attributes);
+ Enumeration<String> keys = ad.keys();
+ while (keys.hasMoreElements()) {
+ String key = keys.nextElement();
+ dictionary.put(key, ad.get(key));
+ }
+ }
+
+ /**
+ * Copy a {@link Dictionary} into an {@link Attributes}.
+ */
+ public static void copy(Dictionary<String, Object> dictionary, Attributes attributes) {
+ AttributesDictionary ad = new AttributesDictionary(attributes);
+ Enumeration<String> keys = dictionary.keys();
+ while (keys.hasMoreElements()) {
+ String key = keys.nextElement();
+ ad.put(key, dictionary.get(key));
+ }
+ }
+}
--- /dev/null
+package org.argeo.naming;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.StringTokenizer;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import org.argeo.osgi.useradmin.UserDirectoryException;
+
+/** LDAP authPassword field according to RFC 3112 */
+public class AuthPassword implements CallbackHandler {
+ private final String authScheme;
+ private final String authInfo;
+ private final String authValue;
+
+ public AuthPassword(String value) {
+ StringTokenizer st = new StringTokenizer(value, "$");
+ // TODO make it more robust, deal with bad formatting
+ this.authScheme = st.nextToken().trim();
+ this.authInfo = st.nextToken().trim();
+ this.authValue = st.nextToken().trim();
+
+ String expectedAuthScheme = getExpectedAuthScheme();
+ if (expectedAuthScheme != null && !authScheme.equals(expectedAuthScheme))
+ throw new IllegalArgumentException(
+ "Auth scheme " + authScheme + " is not compatible with " + expectedAuthScheme);
+ }
+
+ protected AuthPassword(String authInfo, String authValue) {
+ this.authScheme = getExpectedAuthScheme();
+ if (authScheme == null)
+ throw new IllegalArgumentException("Expected auth scheme cannot be null");
+ this.authInfo = authInfo;
+ this.authValue = authValue;
+ }
+
+ protected AuthPassword(AuthPassword authPassword) {
+ this.authScheme = authPassword.getAuthScheme();
+ this.authInfo = authPassword.getAuthInfo();
+ this.authValue = authPassword.getAuthValue();
+ }
+
+ protected String getExpectedAuthScheme() {
+ return null;
+ }
+
+ protected boolean matchAuthValue(Object object) {
+ return authValue.equals(object.toString());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof AuthPassword))
+ return false;
+ AuthPassword authPassword = (AuthPassword) obj;
+ return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo)
+ && authValue.equals(authValue);
+ }
+
+ public boolean keyEquals(AuthPassword authPassword) {
+ return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo);
+ }
+
+ @Override
+ public int hashCode() {
+ return authValue.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return toAuthPassword();
+ }
+
+ public final String toAuthPassword() {
+ return getAuthScheme() + '$' + authInfo + '$' + authValue;
+ }
+
+ public String getAuthScheme() {
+ return authScheme;
+ }
+
+ public String getAuthInfo() {
+ return authInfo;
+ }
+
+ public String getAuthValue() {
+ return authValue;
+ }
+
+ public static AuthPassword matchAuthValue(Attributes attributes, char[] value) {
+ try {
+ Attribute authPassword = attributes.get(LdapAttrs.authPassword.name());
+ if (authPassword != null) {
+ NamingEnumeration<?> values = authPassword.getAll();
+ while (values.hasMore()) {
+ Object val = values.next();
+ AuthPassword token = new AuthPassword(val.toString());
+ String auth;
+ if (Arrays.binarySearch(value, '$') >= 0) {
+ auth = token.authInfo + '$' + token.authValue;
+ } else {
+ auth = token.authValue;
+ }
+ if (Arrays.equals(auth.toCharArray(), value))
+ return token;
+ // if (token.matchAuthValue(value))
+ // return token;
+ }
+ }
+ return null;
+ } catch (NamingException e) {
+ throw new UserDirectoryException("Cannot check attribute", e);
+ }
+ }
+
+ public static boolean remove(Attributes attributes, AuthPassword value) {
+ Attribute authPassword = attributes.get(LdapAttrs.authPassword.name());
+ return authPassword.remove(value.toAuthPassword());
+ }
+
+ @Override
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+ for (Callback callback : callbacks) {
+ if (callback instanceof NameCallback)
+ ((NameCallback) callback).setName(toAuthPassword());
+ else if (callback instanceof PasswordCallback)
+ ((PasswordCallback) callback).setPassword(getAuthValue().toCharArray());
+ }
+ }
+
+}
--- /dev/null
+package org.argeo.naming;
+
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+/**
+ * An object that can be identified with an X.500 distinguished name.
+ *
+ * @see https://tools.ietf.org/html/rfc1779
+ */
+public interface Distinguished {
+ /** The related distinguished name. */
+ String dn();
+
+ /** The related distinguished name as an {@link LdapName}. */
+ default LdapName distinguishedName() {
+ try {
+ return new LdapName(dn());
+ } catch (InvalidNameException e) {
+ throw new IllegalArgumentException("Distinguished name " + dn() + " is not properly formatted.", e);
+ }
+ }
+
+ /** List all DNs of an enumeration as strings. */
+ static Set<String> enumToDns(EnumSet<? extends Distinguished> enumSet) {
+ Set<String> res = new TreeSet<>();
+ for (Enum<? extends Distinguished> enm : enumSet) {
+ res.add(((Distinguished) enm).dn());
+ }
+ return res;
+ }
+}
--- /dev/null
+package org.argeo.naming;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import javax.naming.Binding;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+
+public class DnsBrowser implements Closeable {
+ private final DirContext initialCtx;
+
+ public DnsBrowser() throws NamingException {
+ this(null);
+ }
+
+ public DnsBrowser(String dnsServerUrls) throws NamingException {
+ Hashtable<String, Object> env = new Hashtable<>();
+ env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
+ if (dnsServerUrls != null)
+ env.put("java.naming.provider.url", dnsServerUrls);
+ initialCtx = new InitialDirContext(env);
+ }
+
+ public Map<String, List<String>> getAllRecords(String name) throws NamingException {
+ Map<String, List<String>> res = new TreeMap<>();
+ Attributes attrs = initialCtx.getAttributes(name);
+ NamingEnumeration<String> ids = attrs.getIDs();
+ while (ids.hasMore()) {
+ String recordType = ids.next();
+ List<String> lst = new ArrayList<String>();
+ res.put(recordType, lst);
+ Attribute attr = attrs.get(recordType);
+ addValues(attr, lst);
+ }
+ return Collections.unmodifiableMap(res);
+ }
+
+ /**
+ * Return a single record (typically A, AAAA, etc. or null if not available.
+ * Will fail if multiple records.
+ */
+ public String getRecord(String name, String recordType) throws NamingException {
+ Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType });
+ if (attrs.size() == 0)
+ return null;
+ Attribute attr = attrs.get(recordType);
+ if (attr.size() > 1)
+ throw new IllegalArgumentException("Multiple record type " + recordType);
+ assert attr.size() != 0;
+ Object value = attr.get();
+ assert value != null;
+ return value.toString();
+ }
+
+ /**
+ * Return records of a given type.
+ */
+ public List<String> getRecords(String name, String recordType) throws NamingException {
+ List<String> res = new ArrayList<String>();
+ Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType });
+ Attribute attr = attrs.get(recordType);
+ addValues(attr, res);
+ return res;
+ }
+
+ /** Ordered, with preferred first. */
+ public List<String> getSrvRecordsAsHosts(String name, boolean withPort) throws NamingException {
+ List<String> raw = getRecords(name, "SRV");
+ if (raw.size() == 0)
+ return null;
+ SortedSet<SrvRecord> res = new TreeSet<>();
+ for (int i = 0; i < raw.size(); i++) {
+ String record = raw.get(i);
+ String[] arr = record.split(" ");
+ Integer priority = Integer.parseInt(arr[0]);
+ Integer weight = Integer.parseInt(arr[1]);
+ Integer port = Integer.parseInt(arr[2]);
+ String hostname = arr[3];
+ SrvRecord order = new SrvRecord(priority, weight, port, hostname);
+ res.add(order);
+ }
+ List<String> lst = new ArrayList<>();
+ for (SrvRecord order : res) {
+ lst.add(order.toHost(withPort));
+ }
+ return Collections.unmodifiableList(lst);
+ }
+
+ private void addValues(Attribute attr, List<String> lst) throws NamingException {
+ NamingEnumeration<?> values = attr.getAll();
+ while (values.hasMore()) {
+ Object value = values.next();
+ if (value != null) {
+ if (value instanceof byte[]) {
+ String str = Base64.getEncoder().encodeToString((byte[]) value);
+ lst.add(str);
+ } else
+ lst.add(value.toString());
+ }
+ }
+
+ }
+
+ public List<String> listEntries(String name) throws NamingException {
+ List<String> res = new ArrayList<String>();
+ NamingEnumeration<Binding> ne = initialCtx.listBindings(name);
+ while (ne.hasMore()) {
+ Binding b = ne.next();
+ res.add(b.getName());
+ }
+ return Collections.unmodifiableList(res);
+ }
+
+ @Override
+ public void close() throws IOException {
+ destroy();
+ }
+
+ public void destroy() {
+ try {
+ initialCtx.close();
+ } catch (NamingException e) {
+ // silent
+ }
+ }
+
+ public static void main(String[] args) {
+ if (args.length == 0) {
+ printUsage(System.err);
+ System.exit(1);
+ }
+ try (DnsBrowser dnsBrowser = new DnsBrowser()) {
+ String hostname = args[0];
+ String recordType = args.length > 1 ? args[1] : "A";
+ if (recordType.equals("*")) {
+ Map<String, List<String>> records = dnsBrowser.getAllRecords(hostname);
+ for (String type : records.keySet()) {
+ for (String record : records.get(type)) {
+ String typeLabel;
+ if ("44".equals(type))
+ typeLabel = "SSHFP";
+ else if ("46".equals(type))
+ typeLabel = "RRSIG";
+ else if ("48".equals(type))
+ typeLabel = "DNSKEY";
+ else
+ typeLabel = type;
+ System.out.println(typeLabel + "\t" + record);
+ }
+ }
+ } else {
+ System.out.println(dnsBrowser.getRecord(hostname, recordType));
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void printUsage(PrintStream out) {
+ out.println("java org.argeo.naming.DnsBrowser <hostname> [<record type> | *]");
+ }
+
+}
\ No newline at end of file
--- /dev/null
+uid,,,0.9.2342.19200300.100.1.1,,RFC 4519
+mail,,,0.9.2342.19200300.100.1.3,,RFC 4524
+info,,,0.9.2342.19200300.100.1.4,,RFC 4524
+drink,,,0.9.2342.19200300.100.1.5,,RFC 4524
+roomNumber,,,0.9.2342.19200300.100.1.6,,RFC 4524
+photo,,,0.9.2342.19200300.100.1.7,,RFC 2798
+userClass,,,0.9.2342.19200300.100.1.8,,RFC 4524
+host,,,0.9.2342.19200300.100.1.9,,RFC 4524
+manager,,,0.9.2342.19200300.100.1.10,,RFC 4524
+documentIdentifier,,,0.9.2342.19200300.100.1.11,,RFC 4524
+documentTitle,,,0.9.2342.19200300.100.1.12,,RFC 4524
+documentVersion,,,0.9.2342.19200300.100.1.13,,RFC 4524
+documentAuthor,,,0.9.2342.19200300.100.1.14,,RFC 4524
+documentLocation,,,0.9.2342.19200300.100.1.15,,RFC 4524
+homePhone,,,0.9.2342.19200300.100.1.20,,RFC 4524
+secretary,,,0.9.2342.19200300.100.1.21,,RFC 4524
+dc,,,0.9.2342.19200300.100.1.25,,RFC 4519
+associatedDomain,,,0.9.2342.19200300.100.1.37,,RFC 4524
+associatedName,,,0.9.2342.19200300.100.1.38,,RFC 4524
+homePostalAddress,,,0.9.2342.19200300.100.1.39,,RFC 4524
+personalTitle,,,0.9.2342.19200300.100.1.40,,RFC 4524
+mobile,,,0.9.2342.19200300.100.1.41,,RFC 4524
+pager,,,0.9.2342.19200300.100.1.42,,RFC 4524
+co,,,0.9.2342.19200300.100.1.43,,RFC 4524
+uniqueIdentifier,,,0.9.2342.19200300.100.1.44,,RFC 4524
+organizationalStatus,,,0.9.2342.19200300.100.1.45,,RFC 4524
+buildingName,,,0.9.2342.19200300.100.1.48,,RFC 4524
+audio,,,0.9.2342.19200300.100.1.55,,RFC 2798
+documentPublisher,,,0.9.2342.19200300.100.1.56,,RFC 4524
+jpegPhoto,,,0.9.2342.19200300.100.1.60,,RFC 2798
+vendorName,,,1.3.6.1.1.4,,RFC 3045
+vendorVersion,,,1.3.6.1.1.5,,RFC 3045
+entryUUID,,,1.3.6.1.1.16.4,,RFC 4530
+entryDN,,,1.3.6.1.1.20,,RFC 5020
+labeledURI,,,1.3.6.1.4.1.250.1.57,,RFC 2798
+numSubordinates,,,1.3.6.1.4.1.453.16.2.103,,draft-ietf-boreham-numsubordinates
+namingContexts,,,1.3.6.1.4.1.1466.101.120.5,,RFC 4512
+altServer,,,1.3.6.1.4.1.1466.101.120.6,,RFC 4512
+supportedExtension,,,1.3.6.1.4.1.1466.101.120.7,,RFC 4512
+supportedControl,,,1.3.6.1.4.1.1466.101.120.13,,RFC 4512
+supportedSASLMechanisms,,,1.3.6.1.4.1.1466.101.120.14,,RFC 4512
+supportedLDAPVersion,,,1.3.6.1.4.1.1466.101.120.15,,RFC 4512
+ldapSyntaxes,,,1.3.6.1.4.1.1466.101.120.16,,RFC 4512
+supportedAuthPasswordSchemes,,,1.3.6.1.4.1.4203.1.3.3,,RFC 3112
+authPassword,,,1.3.6.1.4.1.4203.1.3.4,,RFC 3112
+supportedFeatures,,,1.3.6.1.4.1.4203.1.3.5,,RFC 4512
+inheritable,,,1.3.6.1.4.1.7628.5.4.1,,draft-ietf-ldup-subentry
+blockInheritance,,,1.3.6.1.4.1.7628.5.4.2,,draft-ietf-ldup-subentry
+objectClass,,,2.5.4.0,,RFC 4512
+aliasedObjectName,,,2.5.4.1,,RFC 4512
+cn,,,2.5.4.3,,RFC 4519
+sn,,,2.5.4.4,,RFC 4519
+serialNumber,,,2.5.4.5,,RFC 4519
+c,,,2.5.4.6,,RFC 4519
+l,,,2.5.4.7,,RFC 4519
+st,,,2.5.4.8,,RFC 4519
+street,,,2.5.4.9,,RFC 4519
+o,,,2.5.4.10,,RFC 4519
+ou,,,2.5.4.11,,RFC 4519
+title,,,2.5.4.12,,RFC 4519
+description,,,2.5.4.13,,RFC 4519
+searchGuide,,,2.5.4.14,,RFC 4519
+businessCategory,,,2.5.4.15,,RFC 4519
+postalAddress,,,2.5.4.16,,RFC 4519
+postalCode,,,2.5.4.17,,RFC 4519
+postOfficeBox,,,2.5.4.18,,RFC 4519
+physicalDeliveryOfficeName,,,2.5.4.19,,RFC 4519
+telephoneNumber,,,2.5.4.20,,RFC 4519
+telexNumber,,,2.5.4.21,,RFC 4519
+teletexTerminalIdentifier,,,2.5.4.22,,RFC 4519
+facsimileTelephoneNumber,,,2.5.4.23,,RFC 4519
+x121Address,,,2.5.4.24,,RFC 4519
+internationalISDNNumber,,,2.5.4.25,,RFC 4519
+registeredAddress,,,2.5.4.26,,RFC 4519
+destinationIndicator,,,2.5.4.27,,RFC 4519
+preferredDeliveryMethod,,,2.5.4.28,,RFC 4519
+member,,,2.5.4.31,,RFC 4519
+owner,,,2.5.4.32,,RFC 4519
+roleOccupant,,,2.5.4.33,,RFC 4519
+seeAlso,,,2.5.4.34,,RFC 4519
+userPassword,,,2.5.4.35,,RFC 4519
+userCertificate,,,2.5.4.36,,RFC 4523
+cACertificate,,,2.5.4.37,,RFC 4523
+authorityRevocationList,,,2.5.4.38,,RFC 4523
+certificateRevocationList,,,2.5.4.39,,RFC 4523
+crossCertificatePair,,,2.5.4.40,,RFC 4523
+name,,,2.5.4.41,,RFC 4519
+givenName,,,2.5.4.42,,RFC 4519
+initials,,,2.5.4.43,,RFC 4519
+generationQualifier,,,2.5.4.44,,RFC 4519
+x500UniqueIdentifier,,,2.5.4.45,,RFC 4519
+dnQualifier,,,2.5.4.46,,RFC 4519
+enhancedSearchGuide,,,2.5.4.47,,RFC 4519
+distinguishedName,,,2.5.4.49,,RFC 4519
+uniqueMember,,,2.5.4.50,,RFC 4519
+houseIdentifier,,,2.5.4.51,,RFC 4519
+supportedAlgorithms,,,2.5.4.52,,RFC 4523
+deltaRevocationList,,,2.5.4.53,,RFC 4523
+createTimestamp,,,2.5.18.1,,RFC 4512
+modifyTimestamp,,,2.5.18.2,,RFC 4512
+creatorsName,,,2.5.18.3,,RFC 4512
+modifiersName,,,2.5.18.4,,RFC 4512
+subschemaSubentry,,,2.5.18.10,,RFC 4512
+dITStructureRules,,,2.5.21.1,,RFC 4512
+dITContentRules,,,2.5.21.2,,RFC 4512
+matchingRules,,,2.5.21.4,,RFC 4512
+attributeTypes,,,2.5.21.5,,RFC 4512
+objectClasses,,,2.5.21.6,,RFC 4512
+nameForms,,,2.5.21.7,,RFC 4512
+matchingRuleUse,,,2.5.21.8,,RFC 4512
+structuralObjectClass,,,2.5.21.9,,RFC 4512
+governingStructureRule,,,2.5.21.10,,RFC 4512
+carLicense,,,2.16.840.1.113730.3.1.1,,RFC 2798
+departmentNumber,,,2.16.840.1.113730.3.1.2,,RFC 2798
+employeeNumber,,,2.16.840.1.113730.3.1.3,,RFC 2798
+employeeType,,,2.16.840.1.113730.3.1.4,,RFC 2798
+changeNumber,,,2.16.840.1.113730.3.1.5,,draft-good-ldap-changelog
+targetDN,,,2.16.840.1.113730.3.1.6,,draft-good-ldap-changelog
+changeType,,,2.16.840.1.113730.3.1.7,,draft-good-ldap-changelog
+changes,,,2.16.840.1.113730.3.1.8,,draft-good-ldap-changelog
+newRDN,,,2.16.840.1.113730.3.1.9,,draft-good-ldap-changelog
+deleteOldRDN,,,2.16.840.1.113730.3.1.10,,draft-good-ldap-changelog
+newSuperior,,,2.16.840.1.113730.3.1.11,,draft-good-ldap-changelog
+ref,,,2.16.840.1.113730.3.1.34,,RFC 3296
+changelog,,,2.16.840.1.113730.3.1.35,,draft-good-ldap-changelog
+preferredLanguage,,,2.16.840.1.113730.3.1.39,,RFC 2798
+userSMIMECertificate,,,2.16.840.1.113730.3.1.40,,RFC 2798
+userPKCS12,,,2.16.840.1.113730.3.1.216,,RFC 2798
+displayName,,,2.16.840.1.113730.3.1.241,,RFC 2798
--- /dev/null
+package org.argeo.naming;
+
+/**
+ * Standard LDAP attributes as per:<br>
+ * - <a href= "https://www.ldap.com/ldap-oid-reference">Standard LDAP</a><br>
+ * - <a href=
+ * "https://github.com/krb5/krb5/blob/master/src/plugins/kdb/ldap/libkdb_ldap/kerberos.schema">Kerberos
+ * LDAP (partial)</a>
+ */
+public enum LdapAttrs implements SpecifiedName {
+ /** */
+ uid("0.9.2342.19200300.100.1.1", "RFC 4519"),
+ /** */
+ mail("0.9.2342.19200300.100.1.3", "RFC 4524"),
+ /** */
+ info("0.9.2342.19200300.100.1.4", "RFC 4524"),
+ /** */
+ drink("0.9.2342.19200300.100.1.5", "RFC 4524"),
+ /** */
+ roomNumber("0.9.2342.19200300.100.1.6", "RFC 4524"),
+ /** */
+ photo("0.9.2342.19200300.100.1.7", "RFC 2798"),
+ /** */
+ userClass("0.9.2342.19200300.100.1.8", "RFC 4524"),
+ /** */
+ host("0.9.2342.19200300.100.1.9", "RFC 4524"),
+ /** */
+ manager("0.9.2342.19200300.100.1.10", "RFC 4524"),
+ /** */
+ documentIdentifier("0.9.2342.19200300.100.1.11", "RFC 4524"),
+ /** */
+ documentTitle("0.9.2342.19200300.100.1.12", "RFC 4524"),
+ /** */
+ documentVersion("0.9.2342.19200300.100.1.13", "RFC 4524"),
+ /** */
+ documentAuthor("0.9.2342.19200300.100.1.14", "RFC 4524"),
+ /** */
+ documentLocation("0.9.2342.19200300.100.1.15", "RFC 4524"),
+ /** */
+ homePhone("0.9.2342.19200300.100.1.20", "RFC 4524"),
+ /** */
+ secretary("0.9.2342.19200300.100.1.21", "RFC 4524"),
+ /** */
+ dc("0.9.2342.19200300.100.1.25", "RFC 4519"),
+ /** */
+ associatedDomain("0.9.2342.19200300.100.1.37", "RFC 4524"),
+ /** */
+ associatedName("0.9.2342.19200300.100.1.38", "RFC 4524"),
+ /** */
+ homePostalAddress("0.9.2342.19200300.100.1.39", "RFC 4524"),
+ /** */
+ personalTitle("0.9.2342.19200300.100.1.40", "RFC 4524"),
+ /** */
+ mobile("0.9.2342.19200300.100.1.41", "RFC 4524"),
+ /** */
+ pager("0.9.2342.19200300.100.1.42", "RFC 4524"),
+ /** */
+ co("0.9.2342.19200300.100.1.43", "RFC 4524"),
+ /** */
+ uniqueIdentifier("0.9.2342.19200300.100.1.44", "RFC 4524"),
+ /** */
+ organizationalStatus("0.9.2342.19200300.100.1.45", "RFC 4524"),
+ /** */
+ buildingName("0.9.2342.19200300.100.1.48", "RFC 4524"),
+ /** */
+ audio("0.9.2342.19200300.100.1.55", "RFC 2798"),
+ /** */
+ documentPublisher("0.9.2342.19200300.100.1.56", "RFC 4524"),
+ /** */
+ jpegPhoto("0.9.2342.19200300.100.1.60", "RFC 2798"),
+ /** */
+ vendorName("1.3.6.1.1.4", "RFC 3045"),
+ /** */
+ vendorVersion("1.3.6.1.1.5", "RFC 3045"),
+ /** */
+ entryUUID("1.3.6.1.1.16.4", "RFC 4530"),
+ /** */
+ entryDN("1.3.6.1.1.20", "RFC 5020"),
+ /** */
+ labeledURI("1.3.6.1.4.1.250.1.57", "RFC 2798"),
+ /** */
+ numSubordinates("1.3.6.1.4.1.453.16.2.103", "draft-ietf-boreham-numsubordinates"),
+ /** */
+ namingContexts("1.3.6.1.4.1.1466.101.120.5", "RFC 4512"),
+ /** */
+ altServer("1.3.6.1.4.1.1466.101.120.6", "RFC 4512"),
+ /** */
+ supportedExtension("1.3.6.1.4.1.1466.101.120.7", "RFC 4512"),
+ /** */
+ supportedControl("1.3.6.1.4.1.1466.101.120.13", "RFC 4512"),
+ /** */
+ supportedSASLMechanisms("1.3.6.1.4.1.1466.101.120.14", "RFC 4512"),
+ /** */
+ supportedLDAPVersion("1.3.6.1.4.1.1466.101.120.15", "RFC 4512"),
+ /** */
+ ldapSyntaxes("1.3.6.1.4.1.1466.101.120.16", "RFC 4512"),
+ /** */
+ supportedAuthPasswordSchemes("1.3.6.1.4.1.4203.1.3.3", "RFC 3112"),
+ /** */
+ authPassword("1.3.6.1.4.1.4203.1.3.4", "RFC 3112"),
+ /** */
+ supportedFeatures("1.3.6.1.4.1.4203.1.3.5", "RFC 4512"),
+ /** */
+ inheritable("1.3.6.1.4.1.7628.5.4.1", "draft-ietf-ldup-subentry"),
+ /** */
+ blockInheritance("1.3.6.1.4.1.7628.5.4.2", "draft-ietf-ldup-subentry"),
+ /** */
+ objectClass("2.5.4.0", "RFC 4512"),
+ /** */
+ aliasedObjectName("2.5.4.1", "RFC 4512"),
+ /** */
+ cn("2.5.4.3", "RFC 4519"),
+ /** */
+ sn("2.5.4.4", "RFC 4519"),
+ /** */
+ serialNumber("2.5.4.5", "RFC 4519"),
+ /** */
+ c("2.5.4.6", "RFC 4519"),
+ /** */
+ l("2.5.4.7", "RFC 4519"),
+ /** */
+ st("2.5.4.8", "RFC 4519"),
+ /** */
+ street("2.5.4.9", "RFC 4519"),
+ /** */
+ o("2.5.4.10", "RFC 4519"),
+ /** */
+ ou("2.5.4.11", "RFC 4519"),
+ /** */
+ title("2.5.4.12", "RFC 4519"),
+ /** */
+ description("2.5.4.13", "RFC 4519"),
+ /** */
+ searchGuide("2.5.4.14", "RFC 4519"),
+ /** */
+ businessCategory("2.5.4.15", "RFC 4519"),
+ /** */
+ postalAddress("2.5.4.16", "RFC 4519"),
+ /** */
+ postalCode("2.5.4.17", "RFC 4519"),
+ /** */
+ postOfficeBox("2.5.4.18", "RFC 4519"),
+ /** */
+ physicalDeliveryOfficeName("2.5.4.19", "RFC 4519"),
+ /** */
+ telephoneNumber("2.5.4.20", "RFC 4519"),
+ /** */
+ telexNumber("2.5.4.21", "RFC 4519"),
+ /** */
+ teletexTerminalIdentifier("2.5.4.22", "RFC 4519"),
+ /** */
+ facsimileTelephoneNumber("2.5.4.23", "RFC 4519"),
+ /** */
+ x121Address("2.5.4.24", "RFC 4519"),
+ /** */
+ internationalISDNNumber("2.5.4.25", "RFC 4519"),
+ /** */
+ registeredAddress("2.5.4.26", "RFC 4519"),
+ /** */
+ destinationIndicator("2.5.4.27", "RFC 4519"),
+ /** */
+ preferredDeliveryMethod("2.5.4.28", "RFC 4519"),
+ /** */
+ member("2.5.4.31", "RFC 4519"),
+ /** */
+ owner("2.5.4.32", "RFC 4519"),
+ /** */
+ roleOccupant("2.5.4.33", "RFC 4519"),
+ /** */
+ seeAlso("2.5.4.34", "RFC 4519"),
+ /** */
+ userPassword("2.5.4.35", "RFC 4519"),
+ /** */
+ userCertificate("2.5.4.36", "RFC 4523"),
+ /** */
+ cACertificate("2.5.4.37", "RFC 4523"),
+ /** */
+ authorityRevocationList("2.5.4.38", "RFC 4523"),
+ /** */
+ certificateRevocationList("2.5.4.39", "RFC 4523"),
+ /** */
+ crossCertificatePair("2.5.4.40", "RFC 4523"),
+ /** */
+ name("2.5.4.41", "RFC 4519"),
+ /** */
+ givenName("2.5.4.42", "RFC 4519"),
+ /** */
+ initials("2.5.4.43", "RFC 4519"),
+ /** */
+ generationQualifier("2.5.4.44", "RFC 4519"),
+ /** */
+ x500UniqueIdentifier("2.5.4.45", "RFC 4519"),
+ /** */
+ dnQualifier("2.5.4.46", "RFC 4519"),
+ /** */
+ enhancedSearchGuide("2.5.4.47", "RFC 4519"),
+ /** */
+ distinguishedName("2.5.4.49", "RFC 4519"),
+ /** */
+ uniqueMember("2.5.4.50", "RFC 4519"),
+ /** */
+ houseIdentifier("2.5.4.51", "RFC 4519"),
+ /** */
+ supportedAlgorithms("2.5.4.52", "RFC 4523"),
+ /** */
+ deltaRevocationList("2.5.4.53", "RFC 4523"),
+ /** */
+ createTimestamp("2.5.18.1", "RFC 4512"),
+ /** */
+ modifyTimestamp("2.5.18.2", "RFC 4512"),
+ /** */
+ creatorsName("2.5.18.3", "RFC 4512"),
+ /** */
+ modifiersName("2.5.18.4", "RFC 4512"),
+ /** */
+ subschemaSubentry("2.5.18.10", "RFC 4512"),
+ /** */
+ dITStructureRules("2.5.21.1", "RFC 4512"),
+ /** */
+ dITContentRules("2.5.21.2", "RFC 4512"),
+ /** */
+ matchingRules("2.5.21.4", "RFC 4512"),
+ /** */
+ attributeTypes("2.5.21.5", "RFC 4512"),
+ /** */
+ objectClasses("2.5.21.6", "RFC 4512"),
+ /** */
+ nameForms("2.5.21.7", "RFC 4512"),
+ /** */
+ matchingRuleUse("2.5.21.8", "RFC 4512"),
+ /** */
+ structuralObjectClass("2.5.21.9", "RFC 4512"),
+ /** */
+ governingStructureRule("2.5.21.10", "RFC 4512"),
+ /** */
+ carLicense("2.16.840.1.113730.3.1.1", "RFC 2798"),
+ /** */
+ departmentNumber("2.16.840.1.113730.3.1.2", "RFC 2798"),
+ /** */
+ employeeNumber("2.16.840.1.113730.3.1.3", "RFC 2798"),
+ /** */
+ employeeType("2.16.840.1.113730.3.1.4", "RFC 2798"),
+ /** */
+ changeNumber("2.16.840.1.113730.3.1.5", "draft-good-ldap-changelog"),
+ /** */
+ targetDN("2.16.840.1.113730.3.1.6", "draft-good-ldap-changelog"),
+ /** */
+ changeType("2.16.840.1.113730.3.1.7", "draft-good-ldap-changelog"),
+ /** */
+ changes("2.16.840.1.113730.3.1.8", "draft-good-ldap-changelog"),
+ /** */
+ newRDN("2.16.840.1.113730.3.1.9", "draft-good-ldap-changelog"),
+ /** */
+ deleteOldRDN("2.16.840.1.113730.3.1.10", "draft-good-ldap-changelog"),
+ /** */
+ newSuperior("2.16.840.1.113730.3.1.11", "draft-good-ldap-changelog"),
+ /** */
+ ref("2.16.840.1.113730.3.1.34", "RFC 3296"),
+ /** */
+ changelog("2.16.840.1.113730.3.1.35", "draft-good-ldap-changelog"),
+ /** */
+ preferredLanguage("2.16.840.1.113730.3.1.39", "RFC 2798"),
+ /** */
+ userSMIMECertificate("2.16.840.1.113730.3.1.40", "RFC 2798"),
+ /** */
+ userPKCS12("2.16.840.1.113730.3.1.216", "RFC 2798"),
+ /** */
+ displayName("2.16.840.1.113730.3.1.241", "RFC 2798"),
+
+ // Sun memberOf
+ memberOf("1.2.840.113556.1.2.102", "389 DS memberOf"),
+
+ // KERBEROS (partial)
+ krbPrincipalName("2.16.840.1.113719.1.301.6.8.1", "Novell Kerberos Schema Definitions"),
+
+ // RFC 2985 and RFC 3039 (partial)
+ dateOfBirth("1.3.6.1.5.5.7.9.1", "RFC 2985"),
+ /** */
+ placeOfBirth("1.3.6.1.5.5.7.9.2", "RFC 2985"),
+ /** */
+ gender("1.3.6.1.5.5.7.9.3", "RFC 2985"),
+ /** */
+ countryOfCitizenship("1.3.6.1.5.5.7.9.4", "RFC 2985"),
+ /** */
+ countryOfResidence("1.3.6.1.5.5.7.9.5", "RFC 2985"),
+ //
+ ;
+
+ public final static String DN = "dn";
+
+// private final static String LDAP_ = "ldap:";
+
+ private final String oid, spec;
+
+ LdapAttrs(String oid, String spec) {
+ this.oid = oid;
+ this.spec = spec;
+ }
+
+ @Override
+ public String getID() {
+ return oid;
+ }
+
+ @Override
+ public String getSpec() {
+ return spec;
+ }
+
+ public String getPrefix() {
+ return prefix();
+ }
+
+ public static String prefix() {
+ return "ldap";
+ }
+
+ public String property() {
+ return qualified();
+ }
+
+ public String qualified() {
+ String prefix = getPrefix();
+ return prefix != null ? prefix + ":" + name() : name();
+ }
+
+ public String getNamespace() {
+ return namespace();
+ }
+
+ public static String namespace() {
+ return "http://www.argeo.org/ns/ldap";
+ }
+
+ @Override
+ public final String toString() {
+ // must return the name
+ return name();
+ }
+
+}
--- /dev/null
+account,,,0.9.2342.19200300.100.4.5,,RFC 4524
+document,,,0.9.2342.19200300.100.4.6,,RFC 4524
+room,,,0.9.2342.19200300.100.4.7,,RFC 4524
+documentSeries,,,0.9.2342.19200300.100.4.9,,RFC 4524
+domain,,,0.9.2342.19200300.100.4.13,,RFC 4524
+rFC822localPart,,,0.9.2342.19200300.100.4.14,,RFC 4524
+domainRelatedObject,,,0.9.2342.19200300.100.4.17,,RFC 4524
+friendlyCountry,,,0.9.2342.19200300.100.4.18,,RFC 4524
+simpleSecurityObject,,,0.9.2342.19200300.100.4.19,,RFC 4524
+uidObject,,,1.3.6.1.1.3.1,,RFC 4519
+extensibleObject,,,1.3.6.1.4.1.1466.101.120.111,,RFC 4512
+dcObject,,,1.3.6.1.4.1.1466.344,,RFC 4519
+authPasswordObject,,,1.3.6.1.4.1.4203.1.4.7,,RFC 3112
+namedObject,,,1.3.6.1.4.1.5322.13.1.1,,draft-howard-namedobject
+inheritableLDAPSubEntry,,,1.3.6.1.4.1.7628.5.6.1.1,,draft-ietf-ldup-subentry
+top,,,2.5.6.0,,RFC 4512
+alias,,,2.5.6.1,,RFC 4512
+country,,,2.5.6.2,,RFC 4519
+locality,,,2.5.6.3,,RFC 4519
+organization,,,2.5.6.4,,RFC 4519
+organizationalUnit,,,2.5.6.5,,RFC 4519
+person,,,2.5.6.6,,RFC 4519
+organizationalPerson,,,2.5.6.7,,RFC 4519
+organizationalRole,,,2.5.6.8,,RFC 4519
+groupOfNames,,,2.5.6.9,,RFC 4519
+residentialPerson,,,2.5.6.10,,RFC 4519
+applicationProcess,,,2.5.6.11,,RFC 4519
+device,,,2.5.6.14,,RFC 4519
+strongAuthenticationUser,,,2.5.6.15,,RFC 4523
+certificationAuthority,,,2.5.6.16,,RFC 4523
+certificationAuthority-V2,,,2.5.6.16.2,,RFC 4523
+groupOfUniqueNames,,,2.5.6.17,,RFC 4519
+userSecurityInformation,,,2.5.6.18,,RFC 4523
+cRLDistributionPoint,,,2.5.6.19,,RFC 4523
+pkiUser,,,2.5.6.21,,RFC 4523
+pkiCA,,,2.5.6.22,,RFC 4523
+deltaCRL,,,2.5.6.23,,RFC 4523
+subschema,,,2.5.20.1,,RFC 4512
+ldapSubEntry,,,2.16.840.1.113719.2.142.6.1.1,,draft-ietf-ldup-subentry
+changeLogEntry,,,2.16.840.1.113730.3.2.1,,draft-good-ldap-changelog
+inetOrgPerson,,,2.16.840.1.113730.3.2.2,,RFC 2798
+referral,,,2.16.840.1.113730.3.2.6,,RFC 3296
--- /dev/null
+package org.argeo.naming;
+
+/**
+ * Standard LDAP object classes as per
+ * <a href="https://www.ldap.com/ldap-oid-reference">https://www.ldap.com/ldap-
+ * oid-reference</a>
+ */
+public enum LdapObjs implements SpecifiedName {
+ account("0.9.2342.19200300.100.4.5", "RFC 4524"),
+ /** */
+ document("0.9.2342.19200300.100.4.6", "RFC 4524"),
+ /** */
+ room("0.9.2342.19200300.100.4.7", "RFC 4524"),
+ /** */
+ documentSeries("0.9.2342.19200300.100.4.9", "RFC 4524"),
+ /** */
+ domain("0.9.2342.19200300.100.4.13", "RFC 4524"),
+ /** */
+ rFC822localPart("0.9.2342.19200300.100.4.14", "RFC 4524"),
+ /** */
+ domainRelatedObject("0.9.2342.19200300.100.4.17", "RFC 4524"),
+ /** */
+ friendlyCountry("0.9.2342.19200300.100.4.18", "RFC 4524"),
+ /** */
+ simpleSecurityObject("0.9.2342.19200300.100.4.19", "RFC 4524"),
+ /** */
+ uidObject("1.3.6.1.1.3.1", "RFC 4519"),
+ /** */
+ extensibleObject("1.3.6.1.4.1.1466.101.120.111", "RFC 4512"),
+ /** */
+ dcObject("1.3.6.1.4.1.1466.344", "RFC 4519"),
+ /** */
+ authPasswordObject("1.3.6.1.4.1.4203.1.4.7", "RFC 3112"),
+ /** */
+ namedObject("1.3.6.1.4.1.5322.13.1.1", "draft-howard-namedobject"),
+ /** */
+ inheritableLDAPSubEntry("1.3.6.1.4.1.7628.5.6.1.1", "draft-ietf-ldup-subentry"),
+ /** */
+ top("2.5.6.0", "RFC 4512"),
+ /** */
+ alias("2.5.6.1", "RFC 4512"),
+ /** */
+ country("2.5.6.2", "RFC 4519"),
+ /** */
+ locality("2.5.6.3", "RFC 4519"),
+ /** */
+ organization("2.5.6.4", "RFC 4519"),
+ /** */
+ organizationalUnit("2.5.6.5", "RFC 4519"),
+ /** */
+ person("2.5.6.6", "RFC 4519"),
+ /** */
+ organizationalPerson("2.5.6.7", "RFC 4519"),
+ /** */
+ organizationalRole("2.5.6.8", "RFC 4519"),
+ /** */
+ groupOfNames("2.5.6.9", "RFC 4519"),
+ /** */
+ residentialPerson("2.5.6.10", "RFC 4519"),
+ /** */
+ applicationProcess("2.5.6.11", "RFC 4519"),
+ /** */
+ device("2.5.6.14", "RFC 4519"),
+ /** */
+ strongAuthenticationUser("2.5.6.15", "RFC 4523"),
+ /** */
+ certificationAuthority("2.5.6.16", "RFC 4523"),
+ // /** Should be certificationAuthority-V2 */
+ // certificationAuthority_V2("2.5.6.16.2", "RFC 4523") {
+ // },
+ /** */
+ groupOfUniqueNames("2.5.6.17", "RFC 4519"),
+ /** */
+ userSecurityInformation("2.5.6.18", "RFC 4523"),
+ /** */
+ cRLDistributionPoint("2.5.6.19", "RFC 4523"),
+ /** */
+ pkiUser("2.5.6.21", "RFC 4523"),
+ /** */
+ pkiCA("2.5.6.22", "RFC 4523"),
+ /** */
+ deltaCRL("2.5.6.23", "RFC 4523"),
+ /** */
+ subschema("2.5.20.1", "RFC 4512"),
+ /** */
+ ldapSubEntry("2.16.840.1.113719.2.142.6.1.1", "draft-ietf-ldup-subentry"),
+ /** */
+ changeLogEntry("2.16.840.1.113730.3.2.1", "draft-good-ldap-changelog"),
+ /** */
+ inetOrgPerson("2.16.840.1.113730.3.2.2", "RFC 2798"),
+ /** */
+ referral("2.16.840.1.113730.3.2.6", "RFC 3296");
+
+ private final static String LDAP_ = "ldap:";
+ private final String oid, spec;
+
+ private LdapObjs(String oid, String spec) {
+ this.oid = oid;
+ this.spec = spec;
+ }
+
+ public String getOid() {
+ return oid;
+ }
+
+ public String getSpec() {
+ return spec;
+ }
+
+ public String property() {
+ return new StringBuilder(LDAP_).append(name()).toString();
+ }
+
+}
--- /dev/null
+package org.argeo.naming;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.naming.InvalidNameException;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.argeo.osgi.useradmin.UserDirectoryException;
+
+/** Basic LDIF parser. */
+public class LdifParser {
+ private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+
+ protected Attributes addAttributes(SortedMap<LdapName, Attributes> res, int lineNumber, LdapName currentDn,
+ Attributes currentAttributes) {
+ try {
+ Rdn nameRdn = currentDn.getRdn(currentDn.size() - 1);
+ Attribute nameAttr = currentAttributes.get(nameRdn.getType());
+ if (nameAttr == null)
+ currentAttributes.put(nameRdn.getType(), nameRdn.getValue());
+ else if (!nameAttr.get().equals(nameRdn.getValue()))
+ throw new UserDirectoryException(
+ "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + currentDn
+ + " (shortly before line " + lineNumber + " in LDIF file)");
+ Attributes previous = res.put(currentDn, currentAttributes);
+ return previous;
+ } catch (NamingException e) {
+ throw new UserDirectoryException("Cannot add " + currentDn, e);
+ }
+ }
+
+ /** With UTF-8 charset */
+ public SortedMap<LdapName, Attributes> read(InputStream in) throws IOException {
+ try (Reader reader = new InputStreamReader(in, DEFAULT_CHARSET)) {
+ return read(reader);
+ } finally {
+ try {
+ in.close();
+ } catch (IOException e) {
+ // silent
+ }
+ }
+ }
+
+ /** Will close the reader. */
+ public SortedMap<LdapName, Attributes> read(Reader reader) throws IOException {
+ SortedMap<LdapName, Attributes> res = new TreeMap<LdapName, Attributes>();
+ try {
+ List<String> lines = new ArrayList<>();
+ try (BufferedReader br = new BufferedReader(reader)) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ lines.add(line);
+ }
+ }
+ if (lines.size() == 0)
+ return res;
+ // add an empty new line since the last line is not checked
+ if (!lines.get(lines.size() - 1).equals(""))
+ lines.add("");
+
+ LdapName currentDn = null;
+ Attributes currentAttributes = null;
+ StringBuilder currentEntry = new StringBuilder();
+
+ readLines: for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) {
+ String line = lines.get(lineNumber);
+ boolean isLastLine = false;
+ if (lineNumber == lines.size() - 1)
+ isLastLine = true;
+ if (line.startsWith(" ")) {
+ currentEntry.append(line.substring(1));
+ if (!isLastLine)
+ continue readLines;
+ }
+
+ if (currentEntry.length() != 0 || isLastLine) {
+ // read previous attribute
+ StringBuilder attrId = new StringBuilder(8);
+ boolean isBase64 = false;
+ readAttrId: for (int i = 0; i < currentEntry.length(); i++) {
+ char c = currentEntry.charAt(i);
+ if (c == ':') {
+ if (i + 1 < currentEntry.length() && currentEntry.charAt(i + 1) == ':')
+ isBase64 = true;
+ currentEntry.delete(0, i + (isBase64 ? 2 : 1));
+ break readAttrId;
+ } else {
+ attrId.append(c);
+ }
+ }
+
+ String attributeId = attrId.toString();
+ // TODO should we really trim the end of the string as well?
+ String cleanValueStr = currentEntry.toString().trim();
+ Object attributeValue = isBase64 ? Base64.getDecoder().decode(cleanValueStr) : cleanValueStr;
+
+ // manage DN attributes
+ if (attributeId.equals(LdapAttrs.DN) || isLastLine) {
+ if (currentDn != null) {
+ //
+ // ADD
+ //
+ Attributes previous = addAttributes(res, lineNumber, currentDn, currentAttributes);
+ if (previous != null) {
+// log.warn("There was already an entry with DN " + currentDn
+// + ", which has been discarded by a subsequent one.");
+ }
+ }
+
+ if (attributeId.equals(LdapAttrs.DN))
+ try {
+ currentDn = new LdapName(attributeValue.toString());
+ currentAttributes = new BasicAttributes(true);
+ } catch (InvalidNameException e) {
+// log.error(attributeValue + " not a valid DN, skipping the entry.");
+ currentDn = null;
+ currentAttributes = null;
+ }
+ }
+
+ // store attribute
+ if (currentAttributes != null) {
+ Attribute attribute = currentAttributes.get(attributeId);
+ if (attribute == null) {
+ attribute = new BasicAttribute(attributeId);
+ currentAttributes.put(attribute);
+ }
+ attribute.add(attributeValue);
+ }
+ currentEntry = new StringBuilder();
+ }
+ currentEntry.append(line);
+ }
+ } finally {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ // silent
+ }
+ }
+ return res;
+ }
+}
\ No newline at end of file
--- /dev/null
+package org.argeo.naming;
+
+import static org.argeo.naming.LdapAttrs.DN;
+import static org.argeo.naming.LdapAttrs.member;
+import static org.argeo.naming.LdapAttrs.objectClass;
+import static org.argeo.naming.LdapAttrs.uniqueMember;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.argeo.osgi.useradmin.UserDirectoryException;
+
+/** Basic LDIF writer */
+public class LdifWriter {
+ private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+ private final Writer writer;
+
+ /** Writer must be closed by caller */
+ public LdifWriter(Writer writer) {
+ this.writer = writer;
+ }
+
+ /** Stream must be closed by caller */
+ public LdifWriter(OutputStream out) {
+ this(new OutputStreamWriter(out, DEFAULT_CHARSET));
+ }
+
+ public void writeEntry(LdapName name, Attributes attributes) throws IOException {
+ try {
+ // check consistency
+ Rdn nameRdn = name.getRdn(name.size() - 1);
+ Attribute nameAttr = attributes.get(nameRdn.getType());
+ if (!nameAttr.get().equals(nameRdn.getValue()))
+ throw new UserDirectoryException(
+ "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + name);
+
+ writer.append(DN + ": ").append(name.toString()).append('\n');
+ Attribute objectClassAttr = attributes.get(objectClass.name());
+ if (objectClassAttr != null)
+ writeAttribute(objectClassAttr);
+ attributes: for (NamingEnumeration<? extends Attribute> attrs = attributes.getAll(); attrs.hasMore();) {
+ Attribute attribute = attrs.next();
+ if (attribute.getID().equals(DN) || attribute.getID().equals(objectClass.name()))
+ continue attributes;// skip DN attribute
+ if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name()))
+ continue attributes;// skip member and uniqueMember attributes, so that they are always written last
+ writeAttribute(attribute);
+ }
+ // write member and uniqueMember attributes last
+ for (NamingEnumeration<? extends Attribute> attrs = attributes.getAll(); attrs.hasMore();) {
+ Attribute attribute = attrs.next();
+ if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name()))
+ writeMemberAttribute(attribute);
+ }
+ writer.append('\n');
+ writer.flush();
+ } catch (NamingException e) {
+ throw new UserDirectoryException("Cannot write LDIF", e);
+ }
+ }
+
+ public void write(Map<LdapName, Attributes> entries) throws IOException {
+ for (LdapName dn : entries.keySet())
+ writeEntry(dn, entries.get(dn));
+ }
+
+ protected void writeAttribute(Attribute attribute) throws NamingException, IOException {
+ for (NamingEnumeration<?> attrValues = attribute.getAll(); attrValues.hasMore();) {
+ Object value = attrValues.next();
+ if (value instanceof byte[]) {
+ String encoded = Base64.getEncoder().encodeToString((byte[]) value);
+ writer.append(attribute.getID()).append(":: ").append(encoded).append('\n');
+ } else {
+ writer.append(attribute.getID()).append(": ").append(value.toString()).append('\n');
+ }
+ }
+ }
+
+ protected void writeMemberAttribute(Attribute attribute) throws NamingException, IOException {
+ // Note: duplicate entries will be swallowed
+ SortedSet<String> values = new TreeSet<>();
+ for (NamingEnumeration<?> attrValues = attribute.getAll(); attrValues.hasMore();) {
+ String value = attrValues.next().toString();
+ values.add(value);
+ }
+
+ for (String value : values) {
+ writer.append(attribute.getID()).append(": ").append(value).append('\n');
+ }
+ }
+}
--- /dev/null
+package org.argeo.naming;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class NamingUtils {
+ /** As per https://tools.ietf.org/html/rfc4517#section-3.3.13 */
+ private final static DateTimeFormatter utcLdapDate = DateTimeFormatter.ofPattern("uuuuMMddHHmmssX")
+ .withZone(ZoneOffset.UTC);
+
+ /** @return null if not parseable */
+ public static Instant ldapDateToInstant(String ldapDate) {
+ try {
+ return OffsetDateTime.parse(ldapDate, utcLdapDate).toInstant();
+ } catch (DateTimeParseException e) {
+ return null;
+ }
+ }
+
+ /** @return null if not parseable */
+ public static ZonedDateTime ldapDateToZonedDateTime(String ldapDate) {
+ try {
+ return OffsetDateTime.parse(ldapDate, utcLdapDate).toZonedDateTime();
+ } catch (DateTimeParseException e) {
+ return null;
+ }
+ }
+
+ public static Calendar ldapDateToCalendar(String ldapDate) {
+ OffsetDateTime instant = OffsetDateTime.parse(ldapDate, utcLdapDate);
+ GregorianCalendar calendar = new GregorianCalendar();
+ calendar.set(Calendar.DAY_OF_MONTH, instant.get(ChronoField.DAY_OF_MONTH));
+ calendar.set(Calendar.MONTH, instant.get(ChronoField.MONTH_OF_YEAR));
+ calendar.set(Calendar.YEAR, instant.get(ChronoField.YEAR));
+ return calendar;
+ }
+
+ public static String instantToLdapDate(ZonedDateTime instant) {
+ return utcLdapDate.format(instant.withZoneSameInstant(ZoneOffset.UTC));
+ }
+
+ public static String getQueryValue(Map<String, List<String>> query, String key) {
+ if (!query.containsKey(key))
+ return null;
+ List<String> val = query.get(key);
+ if (val.size() == 1)
+ return val.get(0);
+ else
+ throw new IllegalArgumentException("There are " + val.size() + " value(s) for " + key);
+ }
+
+ public static Map<String, List<String>> queryToMap(URI uri) {
+ return queryToMap(uri.getQuery());
+ }
+
+ private static Map<String, List<String>> queryToMap(String queryPart) {
+ try {
+ final Map<String, List<String>> query_pairs = new LinkedHashMap<String, List<String>>();
+ if (queryPart == null)
+ return query_pairs;
+ final String[] pairs = queryPart.split("&");
+ for (String pair : pairs) {
+ final int idx = pair.indexOf("=");
+ final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name())
+ : pair;
+ if (!query_pairs.containsKey(key)) {
+ query_pairs.put(key, new LinkedList<String>());
+ }
+ final String value = idx > 0 && pair.length() > idx + 1
+ ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name())
+ : null;
+ query_pairs.get(key).add(value);
+ }
+ return query_pairs;
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException("Cannot convert " + queryPart + " to map", e);
+ }
+ }
+
+ private NamingUtils() {
+
+ }
+
+ public static void main(String args[]) {
+ ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC);
+ String str = utcLdapDate.format(now);
+ System.out.println(str);
+ utcLdapDate.parse(str);
+ utcLdapDate.parse("19520512000000Z");
+ }
+}
--- /dev/null
+package org.argeo.naming;
+
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+
+public class SharedSecret extends AuthPassword {
+ public final static String X_SHARED_SECRET = "X-SharedSecret";
+ private final Instant expiry;
+
+ public SharedSecret(String authInfo, String authValue) {
+ super(authInfo, authValue);
+ expiry = null;
+ }
+
+ public SharedSecret(AuthPassword authPassword) {
+ super(authPassword);
+ String authInfo = getAuthInfo();
+ if (authInfo.length() == 16) {
+ expiry = NamingUtils.ldapDateToInstant(authInfo);
+ } else {
+ expiry = null;
+ }
+ }
+
+ public SharedSecret(ZonedDateTime expiryTimestamp, String value) {
+ super(NamingUtils.instantToLdapDate(expiryTimestamp), value);
+ expiry = expiryTimestamp.withZoneSameInstant(ZoneOffset.UTC).toInstant();
+ }
+
+ public SharedSecret(int hours, String value) {
+ this(ZonedDateTime.now().plusHours(hours), value);
+ }
+
+ @Override
+ protected String getExpectedAuthScheme() {
+ return X_SHARED_SECRET;
+ }
+
+ public boolean isExpired() {
+ if (expiry == null)
+ return false;
+ return expiry.isBefore(Instant.now());
+ }
+
+}
--- /dev/null
+package org.argeo.naming;
+
+/**
+ * A name which has been specified and for which an id has been defined
+ * (typically an OID).
+ */
+public interface SpecifiedName {
+ /** The name */
+ String name();
+
+ /** An RFC or the URLof some specification */
+ default String getSpec() {
+ return null;
+ }
+
+ /** Typically an OID */
+ default String getID() {
+ return getClass().getName() + "." + name();
+ }
+}
--- /dev/null
+package org.argeo.naming;
+
+class SrvRecord implements Comparable<SrvRecord> {
+ private final Integer priority;
+ private final Integer weight;
+ private final Integer port;
+ private final String hostname;
+
+ public SrvRecord(Integer priority, Integer weight, Integer port, String hostname) {
+ this.priority = priority;
+ this.weight = weight;
+ this.port = port;
+ this.hostname = hostname;
+ }
+
+ @Override
+ public int compareTo(SrvRecord other) {
+ // https: // en.wikipedia.org/wiki/SRV_record
+ if (priority != other.priority)
+ return priority - other.priority;
+ if (weight != other.weight)
+ return other.weight - other.weight;
+ String host = toHost(false);
+ String otherHost = other.toHost(false);
+ if (host.length() == otherHost.length())
+ return host.compareTo(otherHost);
+ else
+ return host.length() - otherHost.length();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof SrvRecord) {
+ SrvRecord other = (SrvRecord) obj;
+ return priority == other.priority && weight == other.weight && port == other.port
+ && hostname.equals(other.hostname);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return priority + " " + weight;
+ }
+
+ public String toHost(boolean withPort) {
+ String hostStr = hostname;
+ if (hostname.charAt(hostname.length() - 1) == '.')
+ hostStr = hostname.substring(0, hostname.length() - 1);
+ return hostStr + (withPort ? ":" + port : "");
+ }
+}
--- /dev/null
+/** Generic naming and LDAP support. */
+package org.argeo.naming;
\ No newline at end of file
--- /dev/null
+package org.argeo.osgi.internal;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ * Called to gather information about the OSGi runtime. Should not activate
+ * anything else that canonical monitoring services (not creating implicit
+ * APIs), which is the responsibility of higher levels.
+ */
+public class EnterpriseActivator implements BundleActivator {
+
+ @Override
+ public void start(BundleContext context) throws Exception {
+ }
+
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.metatype;
+
+import org.argeo.naming.SpecifiedName;
+import org.osgi.service.metatype.AttributeDefinition;
+
+public interface EnumAD extends SpecifiedName, AttributeDefinition {
+ String name();
+
+ default Object getDefault() {
+ return null;
+ }
+
+ @Override
+ default String getName() {
+ return name();
+ }
+
+ @Override
+ default String getID() {
+ return getClass().getName() + "." + name();
+ }
+
+ @Override
+ default String getDescription() {
+ return null;
+ }
+
+ @Override
+ default int getCardinality() {
+ return 0;
+ }
+
+ @Override
+ default int getType() {
+ return STRING;
+ }
+
+ @Override
+ default String[] getOptionValues() {
+ return null;
+ }
+
+ @Override
+ default String[] getOptionLabels() {
+ return null;
+ }
+
+ @Override
+ default String validate(String value) {
+ return null;
+ }
+
+ @Override
+ default String[] getDefaultValue() {
+ Object value = getDefault();
+ if (value == null)
+ return null;
+ return new String[] { value.toString() };
+ }
+}
--- /dev/null
+package org.argeo.osgi.metatype;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+import org.osgi.service.metatype.AttributeDefinition;
+import org.osgi.service.metatype.ObjectClassDefinition;
+
+public class EnumOCD<T extends Enum<T>> implements ObjectClassDefinition {
+ private final Class<T> enumClass;
+ private String locale;
+
+ public EnumOCD(Class<T> clazz, String locale) {
+ this.enumClass = clazz;
+ this.locale = locale;
+ }
+
+ @Override
+ public String getName() {
+ return null;
+ }
+
+ public String getLocale() {
+ return locale;
+ }
+
+ @Override
+ public String getID() {
+ return enumClass.getName();
+ }
+
+ @Override
+ public String getDescription() {
+ return null;
+ }
+
+ @Override
+ public AttributeDefinition[] getAttributeDefinitions(int filter) {
+ EnumSet<T> set = EnumSet.allOf(enumClass);
+ List<AttributeDefinition> attrs = new ArrayList<>();
+ for (T key : set)
+ attrs.add((AttributeDefinition) key);
+ return attrs.toArray(new AttributeDefinition[attrs.size()]);
+ }
+
+ @Override
+ public InputStream getIcon(int size) throws IOException {
+ return null;
+ }
+
+}
--- /dev/null
+/** OSGi metatype support. */
+package org.argeo.osgi.metatype;
\ No newline at end of file
--- /dev/null
+package org.argeo.osgi.provisioning;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.zip.ZipInputStream;
+
+import org.osgi.service.provisioning.ProvisioningService;
+
+public class SimpleProvisioningService implements ProvisioningService {
+ private Map<String, Object> map = Collections.synchronizedSortedMap(new TreeMap<>());
+
+ public SimpleProvisioningService() {
+ // update count
+ map.put(PROVISIONING_UPDATE_COUNT, 0);
+ }
+
+ @Override
+ public Dictionary<String, Object> getInformation() {
+ return new Information();
+ }
+
+ @Override
+ public synchronized void setInformation(Dictionary<String, ?> info) {
+ map.clear();
+ addInformation(info);
+ }
+
+ @Override
+ public synchronized void addInformation(Dictionary<String, ?> info) {
+ Enumeration<String> e = info.keys();
+ while (e.hasMoreElements()) {
+ String key = e.nextElement();
+ map.put(key, info.get(key));
+ }
+ incrementProvisioningUpdateCount();
+ }
+
+ protected synchronized void incrementProvisioningUpdateCount() {
+ Integer current = (Integer) map.get(PROVISIONING_UPDATE_COUNT);
+ Integer newValue = current + 1;
+ map.put(PROVISIONING_UPDATE_COUNT, newValue);
+ }
+
+ @Override
+ public synchronized void addInformation(ZipInputStream zis) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+
+ class Information extends Dictionary<String, Object> {
+
+ @Override
+ public int size() {
+ return map.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return map.isEmpty();
+ }
+
+ @Override
+ public Enumeration<String> keys() {
+ Iterator<String> it = map.keySet().iterator();
+ return new Enumeration<String>() {
+
+ @Override
+ public boolean hasMoreElements() {
+ return it.hasNext();
+ }
+
+ @Override
+ public String nextElement() {
+ return it.next();
+ }
+
+ };
+ }
+
+ @Override
+ public Enumeration<Object> elements() {
+ Iterator<Object> it = map.values().iterator();
+ return new Enumeration<Object>() {
+
+ @Override
+ public boolean hasMoreElements() {
+ return it.hasNext();
+ }
+
+ @Override
+ public Object nextElement() {
+ return it.next();
+ }
+
+ };
+ }
+
+ @Override
+ public Object get(Object key) {
+ return map.get(key);
+ }
+
+ @Override
+ public Object put(String key, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object remove(Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+}
--- /dev/null
+/** OSGi provisioning support. */
+package org.argeo.osgi.provisioning;
\ No newline at end of file
--- /dev/null
+package org.argeo.osgi.transaction;
+
+/** JTA transaction status. */
+public class JtaStatusAdapter implements TransactionStatusAdapter<Integer> {
+ private static final Integer STATUS_ACTIVE = 0;
+ private static final Integer STATUS_COMMITTED = 3;
+ private static final Integer STATUS_COMMITTING = 8;
+ private static final Integer STATUS_MARKED_ROLLBACK = 1;
+ private static final Integer STATUS_NO_TRANSACTION = 6;
+ private static final Integer STATUS_PREPARED = 2;
+ private static final Integer STATUS_PREPARING = 7;
+ private static final Integer STATUS_ROLLEDBACK = 4;
+ private static final Integer STATUS_ROLLING_BACK = 9;
+// private static final Integer STATUS_UNKNOWN = 5;
+
+ @Override
+ public Integer getActiveStatus() {
+ return STATUS_ACTIVE;
+ }
+
+ @Override
+ public Integer getPreparingStatus() {
+ return STATUS_PREPARING;
+ }
+
+ @Override
+ public Integer getMarkedRollbackStatus() {
+ return STATUS_MARKED_ROLLBACK;
+ }
+
+ @Override
+ public Integer getPreparedStatus() {
+ return STATUS_PREPARED;
+ }
+
+ @Override
+ public Integer getCommittingStatus() {
+ return STATUS_COMMITTING;
+ }
+
+ @Override
+ public Integer getCommittedStatus() {
+ return STATUS_COMMITTED;
+ }
+
+ @Override
+ public Integer getRollingBackStatus() {
+ return STATUS_ROLLING_BACK;
+ }
+
+ @Override
+ public Integer getRolledBackStatus() {
+ return STATUS_ROLLEDBACK;
+ }
+
+ @Override
+ public Integer getNoTransactionStatus() {
+ return STATUS_NO_TRANSACTION;
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.transaction;
+
+/** Internal unchecked rollback exception. */
+class SimpleRollbackException extends RuntimeException {
+ private static final long serialVersionUID = 8055601819719780566L;
+
+ public SimpleRollbackException() {
+ super();
+ }
+
+ public SimpleRollbackException(Throwable cause) {
+ super(cause);
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.transaction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.transaction.xa.XAException;
+import javax.transaction.xa.XAResource;
+import javax.transaction.xa.Xid;
+
+/** Simple implementation of an XA transaction. */
+class SimpleTransaction<T>
+//implements Transaction, Status
+{
+ private final Xid xid;
+ private T status;
+ private final List<XAResource> xaResources = new ArrayList<XAResource>();
+
+ private final SimpleTransactionManager transactionManager;
+ private TransactionStatusAdapter<T> tsa;
+
+ public SimpleTransaction(SimpleTransactionManager transactionManager, TransactionStatusAdapter<T> tsa) {
+ this.tsa = tsa;
+ this.status = tsa.getActiveStatus();
+ this.xid = new UuidXid();
+ this.transactionManager = transactionManager;
+ }
+
+ public synchronized void commit()
+// throws RollbackException, HeuristicMixedException, HeuristicRollbackException,
+// SecurityException, IllegalStateException, SystemException
+ {
+ status = tsa.getPreparingStatus();
+ for (XAResource xaRes : xaResources) {
+ if (status.equals(tsa.getMarkedRollbackStatus()))
+ break;
+ try {
+ xaRes.prepare(xid);
+ } catch (XAException e) {
+ status = tsa.getMarkedRollbackStatus();
+ error("Cannot prepare " + xaRes + " for " + xid, e);
+ }
+ }
+ if (status.equals(tsa.getMarkedRollbackStatus())) {
+ rollback();
+ throw new SimpleRollbackException();
+ }
+ status = tsa.getPreparedStatus();
+
+ status = tsa.getCommittingStatus();
+ for (XAResource xaRes : xaResources) {
+ if (status.equals(tsa.getMarkedRollbackStatus()))
+ break;
+ try {
+ xaRes.commit(xid, false);
+ } catch (XAException e) {
+ status = tsa.getMarkedRollbackStatus();
+ error("Cannot prepare " + xaRes + " for " + xid, e);
+ }
+ }
+ if (status.equals(tsa.getMarkedRollbackStatus())) {
+ rollback();
+ throw new SimpleRollbackException();
+ }
+
+ // complete
+ status = tsa.getCommittedStatus();
+ clearResources(XAResource.TMSUCCESS);
+ transactionManager.unregister(xid);
+ }
+
+ public synchronized void rollback()
+// throws IllegalStateException, SystemException
+ {
+ status = tsa.getRollingBackStatus();
+ for (XAResource xaRes : xaResources) {
+ try {
+ xaRes.rollback(xid);
+ } catch (XAException e) {
+ error("Cannot rollback " + xaRes + " for " + xid, e);
+ }
+ }
+
+ // complete
+ status = tsa.getRolledBackStatus();
+ clearResources(XAResource.TMFAIL);
+ transactionManager.unregister(xid);
+ }
+
+ public synchronized boolean enlistResource(XAResource xaRes)
+// throws RollbackException, IllegalStateException, SystemException
+ {
+ if (xaResources.add(xaRes)) {
+ try {
+ xaRes.start(getXid(), XAResource.TMNOFLAGS);
+ return true;
+ } catch (XAException e) {
+ error("Cannot enlist " + xaRes, e);
+ return false;
+ }
+ } else
+ return false;
+ }
+
+ public synchronized boolean delistResource(XAResource xaRes, int flag)
+// throws IllegalStateException, SystemException
+ {
+ if (xaResources.remove(xaRes)) {
+ try {
+ xaRes.end(getXid(), flag);
+ } catch (XAException e) {
+ error("Cannot delist " + xaRes, e);
+ return false;
+ }
+ return true;
+ } else
+ return false;
+ }
+
+ protected void clearResources(int flag) {
+ for (XAResource xaRes : xaResources)
+ try {
+ xaRes.end(getXid(), flag);
+ } catch (XAException e) {
+ error("Cannot end " + xaRes, e);
+ }
+ xaResources.clear();
+ }
+
+ protected void error(Object obj, Exception e) {
+ System.err.println(obj);
+ e.printStackTrace();
+ }
+
+ public synchronized T getStatus()
+// throws SystemException
+ {
+ return status;
+ }
+
+// public void registerSynchronization(Synchronization sync)
+// throws RollbackException, IllegalStateException, SystemException {
+// throw new UnsupportedOperationException();
+// }
+
+ public void setRollbackOnly()
+// throws IllegalStateException, SystemException
+ {
+ status = tsa.getMarkedRollbackStatus();
+ }
+
+ @Override
+ public int hashCode() {
+ return xid.hashCode();
+ }
+
+ Xid getXid() {
+ return xid;
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.transaction;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import javax.transaction.xa.XAResource;
+import javax.transaction.xa.Xid;
+
+/**
+ * Simple implementation of an XA transaction manager.
+ */
+public class SimpleTransactionManager
+// implements TransactionManager, UserTransaction
+ implements WorkControl, WorkTransaction {
+ private ThreadLocal<SimpleTransaction<Integer>> current = new ThreadLocal<SimpleTransaction<Integer>>();
+
+ private Map<Xid, SimpleTransaction<Integer>> knownTransactions = Collections
+ .synchronizedMap(new HashMap<Xid, SimpleTransaction<Integer>>());
+ private TransactionStatusAdapter<Integer> tsa = new JtaStatusAdapter();
+// private SyncRegistry syncRegistry = new SyncRegistry();
+
+ /*
+ * WORK IMPLEMENTATION
+ */
+ @Override
+ public <T> T required(Callable<T> work) {
+ T res;
+ begin();
+ try {
+ res = work.call();
+ commit();
+ } catch (Exception e) {
+ rollback();
+ throw new SimpleRollbackException(e);
+ }
+ return res;
+ }
+
+ @Override
+ public WorkContext getWorkContext() {
+ return new WorkContext() {
+
+ @Override
+ public void registerXAResource(XAResource resource, String recoveryId) {
+ getTransaction().enlistResource(resource);
+ }
+ };
+ }
+
+ /*
+ * WORK TRANSACTION IMPLEMENTATION
+ */
+
+ @Override
+ public boolean isNoTransactionStatus() {
+ return tsa.getNoTransactionStatus().equals(getStatus());
+ }
+
+ /*
+ * JTA IMPLEMENTATION
+ */
+
+ public void begin()
+// throws NotSupportedException, SystemException
+ {
+ if (getCurrent() != null)
+ throw new UnsupportedOperationException("Nested transactions are not supported");
+ SimpleTransaction<Integer> transaction = new SimpleTransaction<Integer>(this, tsa);
+ knownTransactions.put(transaction.getXid(), transaction);
+ current.set(transaction);
+ }
+
+ public void commit()
+// throws RollbackException, HeuristicMixedException, HeuristicRollbackException,
+// SecurityException, IllegalStateException, SystemException
+ {
+ if (getCurrent() == null)
+ throw new IllegalStateException("No transaction registered with the current thread.");
+ getCurrent().commit();
+ }
+
+ public int getStatus()
+// throws SystemException
+ {
+ if (getCurrent() == null)
+ return tsa.getNoTransactionStatus();
+ return getTransaction().getStatus();
+ }
+
+ public SimpleTransaction<Integer> getTransaction()
+// throws SystemException
+ {
+ return getCurrent();
+ }
+
+ protected SimpleTransaction<Integer> getCurrent()
+// throws SystemException
+ {
+ SimpleTransaction<Integer> transaction = current.get();
+ if (transaction == null)
+ return null;
+ Integer status = transaction.getStatus();
+ if (status.equals(tsa.getCommittedStatus()) || status.equals(tsa.getRolledBackStatus())) {
+ current.remove();
+ return null;
+ }
+ return transaction;
+ }
+
+ void unregister(Xid xid) {
+ knownTransactions.remove(xid);
+ }
+
+ public void resume(SimpleTransaction<Integer> tobj)
+// throws InvalidTransactionException, IllegalStateException, SystemException
+ {
+ if (getCurrent() != null)
+ throw new IllegalStateException("Transaction " + current.get() + " already registered");
+ current.set(tobj);
+ }
+
+ public void rollback()
+// throws IllegalStateException, SecurityException, SystemException
+ {
+ if (getCurrent() == null)
+ throw new IllegalStateException("No transaction registered with the current thread.");
+ getCurrent().rollback();
+ }
+
+ public void setRollbackOnly()
+// throws IllegalStateException, SystemException
+ {
+ if (getCurrent() == null)
+ throw new IllegalStateException("No transaction registered with the current thread.");
+ getCurrent().setRollbackOnly();
+ }
+
+ public void setTransactionTimeout(int seconds)
+// throws SystemException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ public SimpleTransaction<Integer> suspend()
+// throws SystemException
+ {
+ SimpleTransaction<Integer> transaction = getCurrent();
+ current.remove();
+ return transaction;
+ }
+
+// public TransactionSynchronizationRegistry getTsr() {
+// return syncRegistry;
+// }
+//
+// private class SyncRegistry implements TransactionSynchronizationRegistry {
+// @Override
+// public Object getTransactionKey() {
+// try {
+// SimpleTransaction transaction = getCurrent();
+// if (transaction == null)
+// return null;
+// return getCurrent().getXid();
+// } catch (SystemException e) {
+// throw new IllegalStateException("Cannot get transaction key", e);
+// }
+// }
+//
+// @Override
+// public void putResource(Object key, Object value) {
+// throw new UnsupportedOperationException();
+// }
+//
+// @Override
+// public Object getResource(Object key) {
+// throw new UnsupportedOperationException();
+// }
+//
+// @Override
+// public void registerInterposedSynchronization(Synchronization sync) {
+// throw new UnsupportedOperationException();
+// }
+//
+// @Override
+// public int getTransactionStatus() {
+// try {
+// return getStatus();
+// } catch (SystemException e) {
+// throw new IllegalStateException("Cannot get status", e);
+// }
+// }
+//
+// @Override
+// public boolean getRollbackOnly() {
+// try {
+// return getStatus() == Status.STATUS_MARKED_ROLLBACK;
+// } catch (SystemException e) {
+// throw new IllegalStateException("Cannot get status", e);
+// }
+// }
+//
+// @Override
+// public void setRollbackOnly() {
+// try {
+// getCurrent().setRollbackOnly();
+// } catch (Exception e) {
+// throw new IllegalStateException("Cannot set rollback only", e);
+// }
+// }
+//
+// }
+}
--- /dev/null
+package org.argeo.osgi.transaction;
+
+/** Abstract the various approaches to represent transaction status. */
+public interface TransactionStatusAdapter<T> {
+ T getActiveStatus();
+
+ T getPreparingStatus();
+
+ T getMarkedRollbackStatus();
+
+ T getPreparedStatus();
+
+ T getCommittingStatus();
+
+ T getCommittedStatus();
+
+ T getRollingBackStatus();
+
+ T getRolledBackStatus();
+
+ T getNoTransactionStatus();
+}
--- /dev/null
+package org.argeo.osgi.transaction;
+
+import java.io.Serializable;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.UUID;
+
+import javax.transaction.xa.Xid;
+
+/**
+ * Implementation of {@link Xid} based on {@link UUID}, using max significant
+ * bits as global transaction id, and least significant bits as branch
+ * qualifier.
+ */
+public class UuidXid implements Xid, Serializable {
+ private static final long serialVersionUID = -5380531989917886819L;
+ public final static int FORMAT = (int) serialVersionUID;
+
+ private final static int BYTES_PER_LONG = Long.SIZE / Byte.SIZE;
+
+ private final int format;
+ private final byte[] globalTransactionId;
+ private final byte[] branchQualifier;
+ private final String uuid;
+ private final int hashCode;
+
+ public UuidXid() {
+ this(UUID.randomUUID());
+ }
+
+ public UuidXid(UUID uuid) {
+ this.format = FORMAT;
+ this.globalTransactionId = uuidToBytes(uuid.getMostSignificantBits());
+ this.branchQualifier = uuidToBytes(uuid.getLeastSignificantBits());
+ this.uuid = uuid.toString();
+ this.hashCode = uuid.hashCode();
+ }
+
+ public UuidXid(Xid xid) {
+ this(xid.getFormatId(), xid.getGlobalTransactionId(), xid
+ .getBranchQualifier());
+ }
+
+ private UuidXid(int format, byte[] globalTransactionId,
+ byte[] branchQualifier) {
+ this.format = format;
+ this.globalTransactionId = globalTransactionId;
+ this.branchQualifier = branchQualifier;
+ this.uuid = bytesToUUID(globalTransactionId, branchQualifier)
+ .toString();
+ this.hashCode = uuid.hashCode();
+ }
+
+ @Override
+ public int getFormatId() {
+ return format;
+ }
+
+ @Override
+ public byte[] getGlobalTransactionId() {
+ return Arrays.copyOf(globalTransactionId, globalTransactionId.length);
+ }
+
+ @Override
+ public byte[] getBranchQualifier() {
+ return Arrays.copyOf(branchQualifier, branchQualifier.length);
+ }
+
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj instanceof UuidXid) {
+ UuidXid that = (UuidXid) obj;
+ return Arrays.equals(globalTransactionId, that.globalTransactionId)
+ && Arrays.equals(branchQualifier, that.branchQualifier);
+ }
+ if (obj instanceof Xid) {
+ Xid that = (Xid) obj;
+ return Arrays.equals(globalTransactionId,
+ that.getGlobalTransactionId())
+ && Arrays
+ .equals(branchQualifier, that.getBranchQualifier());
+ }
+ return uuid.equals(obj.toString());
+ }
+
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ return new UuidXid(format, globalTransactionId, branchQualifier);
+ }
+
+ @Override
+ public String toString() {
+ return uuid;
+ }
+
+ public UUID asUuid() {
+ return bytesToUUID(globalTransactionId, branchQualifier);
+ }
+
+ public static byte[] uuidToBytes(long bits) {
+ ByteBuffer buffer = ByteBuffer.allocate(BYTES_PER_LONG);
+ buffer.putLong(0, bits);
+ return buffer.array();
+ }
+
+ public static UUID bytesToUUID(byte[] most, byte[] least) {
+ if (most.length < BYTES_PER_LONG)
+ most = Arrays.copyOf(most, BYTES_PER_LONG);
+ if (least.length < BYTES_PER_LONG)
+ least = Arrays.copyOf(least, BYTES_PER_LONG);
+ ByteBuffer buffer = ByteBuffer.allocate(2 * BYTES_PER_LONG);
+ buffer.put(most, 0, BYTES_PER_LONG);
+ buffer.put(least, 0, BYTES_PER_LONG);
+ buffer.flip();
+ return new UUID(buffer.getLong(), buffer.getLong());
+ }
+
+ // public static void main(String[] args) {
+ // UUID uuid = UUID.randomUUID();
+ // System.out.println(uuid);
+ // uuid = bytesToUUID(uuidToBytes(uuid.getMostSignificantBits()),
+ // uuidToBytes(uuid.getLeastSignificantBits()));
+ // System.out.println(uuid);
+ // }
+}
--- /dev/null
+package org.argeo.osgi.transaction;
+
+import javax.transaction.xa.XAResource;
+
+/**
+ * A minimalistic interface similar to OSGi transaction context in order to
+ * register XA resources.
+ */
+public interface WorkContext {
+ void registerXAResource(XAResource resource, String recoveryId);
+}
--- /dev/null
+package org.argeo.osgi.transaction;
+
+import java.util.concurrent.Callable;
+
+/**
+ * A minimalistic interface inspired by OSGi transaction control in order to
+ * commit units of work externally.
+ */
+public interface WorkControl {
+ <T> T required(Callable<T> work);
+
+ void setRollbackOnly();
+
+ WorkContext getWorkContext();
+}
--- /dev/null
+package org.argeo.osgi.transaction;
+
+/**
+ * A minimalistic interface inspired by JTA user transaction in order to commit
+ * units of work externally.
+ */
+public interface WorkTransaction {
+ void begin();
+
+ void commit();
+
+ void rollback();
+
+ boolean isNoTransactionStatus();
+}
--- /dev/null
+/** Minimalistic and partial XA transaction manager implementation. */
+package org.argeo.osgi.transaction;
\ No newline at end of file
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import static org.argeo.naming.LdapAttrs.objectClass;
+import static org.argeo.naming.LdapObjs.extensibleObject;
+import static org.argeo.naming.LdapObjs.inetOrgPerson;
+import static org.argeo.naming.LdapObjs.organizationalPerson;
+import static org.argeo.naming.LdapObjs.person;
+import static org.argeo.naming.LdapObjs.top;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.naming.InvalidNameException;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.argeo.naming.LdapAttrs;
+import org.argeo.osgi.transaction.WorkControl;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+/** Base class for a {@link UserDirectory}. */
+public abstract class AbstractUserDirectory implements UserAdmin, UserDirectory {
+ static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name";
+ static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password";
+
+ private final Hashtable<String, Object> properties;
+ private final LdapName baseDn, userBaseDn, groupBaseDn;
+ private final String userObjectClass, userBase, groupObjectClass, groupBase;
+
+ private final boolean readOnly;
+ private final boolean disabled;
+ private final String uri;
+
+ private UserAdmin externalRoles;
+ // private List<String> indexedUserProperties = Arrays
+ // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(),
+ // LdapAttrs.cn.name() });
+
+ private final boolean scoped;
+
+ private String memberAttributeId = "member";
+ private List<String> credentialAttributeIds = Arrays
+ .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
+
+ // Transaction
+// private TransactionManager transactionManager;
+ private WorkControl transactionControl;
+ private WcXaResource xaResource = new WcXaResource(this);
+
+ AbstractUserDirectory(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
+ this.scoped = scoped;
+ properties = new Hashtable<String, Object>();
+ for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
+ String key = keys.nextElement();
+ properties.put(key, props.get(key));
+ }
+
+ if (uriArg != null) {
+ uri = uriArg.toString();
+ // uri from properties is ignored
+ } else {
+ String uriStr = UserAdminConf.uri.getValue(properties);
+ if (uriStr == null)
+ uri = null;
+ else
+ uri = uriStr;
+ }
+
+ userObjectClass = UserAdminConf.userObjectClass.getValue(properties);
+ userBase = UserAdminConf.userBase.getValue(properties);
+ groupObjectClass = UserAdminConf.groupObjectClass.getValue(properties);
+ groupBase = UserAdminConf.groupBase.getValue(properties);
+ try {
+ baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
+ userBaseDn = new LdapName(userBase + "," + baseDn);
+ groupBaseDn = new LdapName(groupBase + "," + baseDn);
+ } catch (InvalidNameException e) {
+ throw new UserDirectoryException("Badly formated base DN " + UserAdminConf.baseDn.getValue(properties), e);
+ }
+ String readOnlyStr = UserAdminConf.readOnly.getValue(properties);
+ if (readOnlyStr == null) {
+ readOnly = readOnlyDefault(uri);
+ properties.put(UserAdminConf.readOnly.name(), Boolean.toString(readOnly));
+ } else
+ readOnly = Boolean.parseBoolean(readOnlyStr);
+ String disabledStr = UserAdminConf.disabled.getValue(properties);
+ if (disabledStr != null)
+ disabled = Boolean.parseBoolean(disabledStr);
+ else
+ disabled = false;
+ }
+
+ /** Returns the groups this user is a direct member of. */
+ protected abstract List<LdapName> getDirectGroups(LdapName dn);
+
+ protected abstract Boolean daoHasRole(LdapName dn);
+
+ protected abstract DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException;
+
+ protected abstract List<DirectoryUser> doGetRoles(Filter f);
+
+ protected abstract AbstractUserDirectory scope(User user);
+
+ public void init() {
+
+ }
+
+ public void destroy() {
+
+ }
+
+ protected boolean isEditing() {
+ return xaResource.wc() != null;
+ }
+
+ protected UserDirectoryWorkingCopy getWorkingCopy() {
+ UserDirectoryWorkingCopy wc = xaResource.wc();
+ if (wc == null)
+ return null;
+ return wc;
+ }
+
+ protected void checkEdit() {
+// Transaction transaction;
+// try {
+// transaction = transactionManager.getTransaction();
+// } catch (SystemException e) {
+// throw new UserDirectoryException("Cannot get transaction", e);
+// }
+// if (transaction == null)
+// throw new UserDirectoryException("A transaction needs to be active in order to edit");
+ if (xaResource.wc() == null) {
+ try {
+// transaction.enlistResource(xaResource);
+ transactionControl.getWorkContext().registerXAResource(xaResource, null);
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot enlist " + xaResource, e);
+ }
+ } else {
+ }
+ }
+
+ protected List<Role> getAllRoles(DirectoryUser user) {
+ List<Role> allRoles = new ArrayList<Role>();
+ if (user != null) {
+ collectRoles(user, allRoles);
+ allRoles.add(user);
+ } else
+ collectAnonymousRoles(allRoles);
+ return allRoles;
+ }
+
+ private void collectRoles(DirectoryUser user, List<Role> allRoles) {
+ Attributes attrs = user.getAttributes();
+ // TODO centralize attribute name
+ Attribute memberOf = attrs.get(LdapAttrs.memberOf.name());
+ // if user belongs to this directory, we only check meberOf
+ if (memberOf != null && user.getDn().startsWith(getBaseDn())) {
+ try {
+ NamingEnumeration<?> values = memberOf.getAll();
+ while (values.hasMore()) {
+ Object value = values.next();
+ LdapName groupDn = new LdapName(value.toString());
+ DirectoryUser group = doGetRole(groupDn);
+ if (group != null)
+ allRoles.add(group);
+ }
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot get memberOf groups for " + user, e);
+ }
+ } else {
+ for (LdapName groupDn : getDirectGroups(user.getDn())) {
+ // TODO check for loops
+ DirectoryUser group = doGetRole(groupDn);
+ if (group != null) {
+ allRoles.add(group);
+ collectRoles(group, allRoles);
+ }
+ }
+ }
+ }
+
+ private void collectAnonymousRoles(List<Role> allRoles) {
+ // TODO gather anonymous roles
+ }
+
+ // USER ADMIN
+ @Override
+ public Role getRole(String name) {
+ return doGetRole(toDn(name));
+ }
+
+ protected DirectoryUser doGetRole(LdapName dn) {
+ UserDirectoryWorkingCopy wc = getWorkingCopy();
+ DirectoryUser user;
+ try {
+ user = daoGetRole(dn);
+ } catch (NameNotFoundException e) {
+ user = null;
+ }
+ if (wc != null) {
+ if (user == null && wc.getNewUsers().containsKey(dn))
+ user = wc.getNewUsers().get(dn);
+ else if (wc.getDeletedUsers().containsKey(dn))
+ user = null;
+ }
+ return user;
+ }
+
+ @Override
+ public Role[] getRoles(String filter) throws InvalidSyntaxException {
+ UserDirectoryWorkingCopy wc = getWorkingCopy();
+ Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
+ List<DirectoryUser> res = doGetRoles(f);
+ if (wc != null) {
+ for (Iterator<DirectoryUser> it = res.iterator(); it.hasNext();) {
+ DirectoryUser user = it.next();
+ LdapName dn = user.getDn();
+ if (wc.getDeletedUsers().containsKey(dn))
+ it.remove();
+ }
+ for (DirectoryUser user : wc.getNewUsers().values()) {
+ if (f == null || f.match(user.getProperties()))
+ res.add(user);
+ }
+ // no need to check modified users,
+ // since doGetRoles was already based on the modified attributes
+ }
+ return res.toArray(new Role[res.size()]);
+ }
+
+ @Override
+ public User getUser(String key, String value) {
+ // TODO check value null or empty
+ List<DirectoryUser> collectedUsers = new ArrayList<DirectoryUser>();
+ if (key != null) {
+ doGetUser(key, value, collectedUsers);
+ } else {
+ throw new UserDirectoryException("Key cannot be null");
+ }
+
+ if (collectedUsers.size() == 1) {
+ return collectedUsers.get(0);
+ } else if (collectedUsers.size() > 1) {
+ // log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" :
+ // "") + value);
+ }
+ return null;
+ }
+
+ protected void doGetUser(String key, String value, List<DirectoryUser> collectedUsers) {
+ try {
+ Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")");
+ List<DirectoryUser> users = doGetRoles(f);
+ collectedUsers.addAll(users);
+ } catch (InvalidSyntaxException e) {
+ throw new UserDirectoryException("Cannot get user with " + key + "=" + value, e);
+ }
+ }
+
+ @Override
+ public Authorization getAuthorization(User user) {
+ if (user == null || user instanceof DirectoryUser) {
+ return new LdifAuthorization(user, getAllRoles((DirectoryUser) user));
+ } else {
+ // bind
+ AbstractUserDirectory scopedUserAdmin = scope(user);
+ try {
+ DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName());
+ if (directoryUser == null)
+ throw new UserDirectoryException("No scoped user found for " + user);
+ LdifAuthorization authorization = new LdifAuthorization(directoryUser,
+ scopedUserAdmin.getAllRoles(directoryUser));
+ return authorization;
+ } finally {
+ scopedUserAdmin.destroy();
+ }
+ }
+ }
+
+ @Override
+ public Role createRole(String name, int type) {
+ checkEdit();
+ UserDirectoryWorkingCopy wc = getWorkingCopy();
+ LdapName dn = toDn(name);
+ if ((daoHasRole(dn) && !wc.getDeletedUsers().containsKey(dn)) || wc.getNewUsers().containsKey(dn))
+ throw new UserDirectoryException("Already a role " + name);
+ BasicAttributes attrs = new BasicAttributes(true);
+ // attrs.put(LdifName.dn.name(), dn.toString());
+ Rdn nameRdn = dn.getRdn(dn.size() - 1);
+ // TODO deal with multiple attr RDN
+ attrs.put(nameRdn.getType(), nameRdn.getValue());
+ if (wc.getDeletedUsers().containsKey(dn)) {
+ wc.getDeletedUsers().remove(dn);
+ wc.getModifiedUsers().put(dn, attrs);
+ return getRole(name);
+ } else {
+ wc.getModifiedUsers().put(dn, attrs);
+ DirectoryUser newRole = newRole(dn, type, attrs);
+ wc.getNewUsers().put(dn, newRole);
+ return newRole;
+ }
+ }
+
+ protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) {
+ LdifUser newRole;
+ BasicAttribute objClass = new BasicAttribute(objectClass.name());
+ if (type == Role.USER) {
+ String userObjClass = newUserObjectClass(dn);
+ objClass.add(userObjClass);
+ if (inetOrgPerson.name().equals(userObjClass)) {
+ objClass.add(organizationalPerson.name());
+ objClass.add(person.name());
+ } else if (organizationalPerson.name().equals(userObjClass)) {
+ objClass.add(person.name());
+ }
+ objClass.add(top.name());
+ objClass.add(extensibleObject.name());
+ attrs.put(objClass);
+ newRole = new LdifUser(this, dn, attrs);
+ } else if (type == Role.GROUP) {
+ String groupObjClass = getGroupObjectClass();
+ objClass.add(groupObjClass);
+ // objClass.add(LdifName.extensibleObject.name());
+ objClass.add(top.name());
+ attrs.put(objClass);
+ newRole = new LdifGroup(this, dn, attrs);
+ } else
+ throw new UserDirectoryException("Unsupported type " + type);
+ return newRole;
+ }
+
+ @Override
+ public boolean removeRole(String name) {
+ checkEdit();
+ UserDirectoryWorkingCopy wc = getWorkingCopy();
+ LdapName dn = toDn(name);
+ boolean actuallyDeleted;
+ if (daoHasRole(dn) || wc.getNewUsers().containsKey(dn)) {
+ DirectoryUser user = (DirectoryUser) getRole(name);
+ wc.getDeletedUsers().put(dn, user);
+ actuallyDeleted = true;
+ } else {// just removing from groups (e.g. system roles)
+ actuallyDeleted = false;
+ }
+ for (LdapName groupDn : getDirectGroups(dn)) {
+ DirectoryUser group = doGetRole(groupDn);
+ group.getAttributes().get(getMemberAttributeId()).remove(dn.toString());
+ }
+ return actuallyDeleted;
+ }
+
+ // TRANSACTION
+ protected void prepare(UserDirectoryWorkingCopy wc) {
+
+ }
+
+ protected void commit(UserDirectoryWorkingCopy wc) {
+
+ }
+
+ protected void rollback(UserDirectoryWorkingCopy wc) {
+
+ }
+
+ // UTILITIES
+ protected LdapName toDn(String name) {
+ try {
+ return new LdapName(name);
+ } catch (InvalidNameException e) {
+ throw new UserDirectoryException("Badly formatted name", e);
+ }
+ }
+
+ // GETTERS
+ protected String getMemberAttributeId() {
+ return memberAttributeId;
+ }
+
+ protected List<String> getCredentialAttributeIds() {
+ return credentialAttributeIds;
+ }
+
+ protected String getUri() {
+ return uri;
+ }
+
+ private static boolean readOnlyDefault(String uriStr) {
+ if (uriStr == null)
+ return true;
+ /// TODO make it more generic
+ URI uri;
+ try {
+ uri = new URI(uriStr.split(" ")[0]);
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ if (uri.getScheme() == null)
+ return false;// assume relative file to be writable
+ if (uri.getScheme().equals(UserAdminConf.SCHEME_FILE)) {
+ File file = new File(uri);
+ if (file.exists())
+ return !file.canWrite();
+ else
+ return !file.getParentFile().canWrite();
+ } else if (uri.getScheme().equals(UserAdminConf.SCHEME_LDAP)) {
+ if (uri.getAuthority() != null)// assume writable if authenticated
+ return false;
+ } else if (uri.getScheme().equals(UserAdminConf.SCHEME_OS)) {
+ return true;
+ }
+ return true;// read only by default
+ }
+
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ public boolean isDisabled() {
+ return disabled;
+ }
+
+ protected UserAdmin getExternalRoles() {
+ return externalRoles;
+ }
+
+ protected int roleType(LdapName dn) {
+ if (dn.startsWith(groupBaseDn))
+ return Role.GROUP;
+ else if (dn.startsWith(userBaseDn))
+ return Role.USER;
+ else
+ return Role.GROUP;
+ }
+
+ /** dn can be null, in that case a default should be returned. */
+ public String getUserObjectClass() {
+ return userObjectClass;
+ }
+
+ public String getUserBase() {
+ return userBase;
+ }
+
+ protected String newUserObjectClass(LdapName dn) {
+ return getUserObjectClass();
+ }
+
+ public String getGroupObjectClass() {
+ return groupObjectClass;
+ }
+
+ public String getGroupBase() {
+ return groupBase;
+ }
+
+ public LdapName getBaseDn() {
+ return (LdapName) baseDn.clone();
+ }
+
+ public Dictionary<String, Object> getProperties() {
+ return properties;
+ }
+
+ public Dictionary<String, Object> cloneProperties() {
+ return new Hashtable<>(properties);
+ }
+
+ public void setExternalRoles(UserAdmin externalRoles) {
+ this.externalRoles = externalRoles;
+ }
+
+// public void setTransactionManager(TransactionManager transactionManager) {
+// this.transactionManager = transactionManager;
+// }
+
+ public void setTransactionControl(WorkControl transactionControl) {
+ this.transactionControl = transactionControl;
+ }
+
+ public WcXaResource getXaResource() {
+ return xaResource;
+ }
+
+ public boolean isScoped() {
+ return scoped;
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.osgi.service.useradmin.Authorization;
+
+/** An {@link Authorization} which combines roles form various auth sources. */
+class AggregatingAuthorization implements Authorization {
+ private final String name;
+ private final String displayName;
+ private final Set<String> systemRoles;
+ private final Set<String> roles;
+
+ public AggregatingAuthorization(String name, String displayName, Set<String> systemRoles, String[] roles) {
+ this.name = new X500Principal(name).getName();
+ this.displayName = displayName;
+ this.systemRoles = Collections.unmodifiableSet(new HashSet<>(systemRoles));
+ Set<String> temp = new HashSet<>();
+ for (String role : roles) {
+ if (!temp.contains(role))
+ temp.add(role);
+ }
+ this.roles = Collections.unmodifiableSet(temp);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean hasRole(String name) {
+ if (systemRoles.contains(name))
+ return true;
+ if (roles.contains(name))
+ return true;
+ return false;
+ }
+
+ @Override
+ public String[] getRoles() {
+ int size = systemRoles.size() + roles.size();
+ List<String> res = new ArrayList<String>(size);
+ res.addAll(systemRoles);
+ res.addAll(roles);
+ return res.toArray(new String[size]);
+ }
+
+ @Override
+ public int hashCode() {
+ if (name == null)
+ return super.hashCode();
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Authorization))
+ return false;
+ Authorization that = (Authorization) obj;
+ if (name == null)
+ return that.getName() == null;
+ return name.equals(that.getName());
+ }
+
+ @Override
+ public String toString() {
+ return displayName;
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.osgi.framework.InvalidSyntaxException;
+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.UserAdmin;
+
+/**
+ * Aggregates multiple {@link UserDirectory} and integrates them with system
+ * roles.
+ */
+public class AggregatingUserAdmin implements UserAdmin {
+ private final LdapName systemRolesBaseDn;
+ private final LdapName tokensBaseDn;
+
+ // DAOs
+ private AbstractUserDirectory systemRoles = null;
+ private AbstractUserDirectory tokens = null;
+ private Map<LdapName, AbstractUserDirectory> businessRoles = new HashMap<LdapName, AbstractUserDirectory>();
+
+ public AggregatingUserAdmin(String systemRolesBaseDn, String tokensBaseDn) {
+ try {
+ this.systemRolesBaseDn = new LdapName(systemRolesBaseDn);
+ if (tokensBaseDn != null)
+ this.tokensBaseDn = new LdapName(tokensBaseDn);
+ else
+ this.tokensBaseDn = null;
+ } catch (InvalidNameException e) {
+ throw new UserDirectoryException("Cannot initialize " + AggregatingUserAdmin.class, e);
+ }
+ }
+
+ @Override
+ public Role createRole(String name, int type) {
+ return findUserAdmin(name).createRole(name, type);
+ }
+
+ @Override
+ public boolean removeRole(String name) {
+ boolean actuallyDeleted = findUserAdmin(name).removeRole(name);
+ systemRoles.removeRole(name);
+ return actuallyDeleted;
+ }
+
+ @Override
+ public Role getRole(String name) {
+ return findUserAdmin(name).getRole(name);
+ }
+
+ @Override
+ public Role[] getRoles(String filter) throws InvalidSyntaxException {
+ List<Role> res = new ArrayList<Role>();
+ for (UserAdmin userAdmin : businessRoles.values()) {
+ res.addAll(Arrays.asList(userAdmin.getRoles(filter)));
+ }
+ res.addAll(Arrays.asList(systemRoles.getRoles(filter)));
+ return res.toArray(new Role[res.size()]);
+ }
+
+ @Override
+ public User getUser(String key, String value) {
+ List<User> res = new ArrayList<User>();
+ for (UserAdmin userAdmin : businessRoles.values()) {
+ User u = userAdmin.getUser(key, value);
+ if (u != null)
+ res.add(u);
+ }
+ // Note: node roles cannot contain users, so it is not searched
+ return res.size() == 1 ? res.get(0) : null;
+ }
+
+ @Override
+ public Authorization getAuthorization(User user) {
+ if (user == null) {// anonymous
+ return systemRoles.getAuthorization(null);
+ }
+ AbstractUserDirectory userReferentialOfThisUser = findUserAdmin(user.getName());
+ Authorization rawAuthorization = userReferentialOfThisUser.getAuthorization(user);
+ String usernameToUse;
+ String displayNameToUse;
+ if (user instanceof Group) {
+ // TODO check whether this is still working
+ String ownerDn = TokenUtils.userDn((Group) user);
+ if (ownerDn != null) {// tokens
+ UserAdmin ownerUserAdmin = findUserAdmin(ownerDn);
+ User ownerUser = (User) ownerUserAdmin.getRole(ownerDn);
+ usernameToUse = ownerDn;
+ displayNameToUse = LdifAuthorization.extractDisplayName(ownerUser);
+ } else {
+ usernameToUse = rawAuthorization.getName();
+ displayNameToUse = rawAuthorization.toString();
+ }
+ } else {// regular users
+ usernameToUse = rawAuthorization.getName();
+ displayNameToUse = rawAuthorization.toString();
+ }
+
+ // gather roles from other referentials
+ final AbstractUserDirectory userAdminToUse;// possibly scoped when authenticating
+ if (user instanceof DirectoryUser) {
+ userAdminToUse = userReferentialOfThisUser;
+ } else if (user instanceof AuthenticatingUser) {
+ userAdminToUse = userReferentialOfThisUser.scope(user);
+ } else {
+ throw new IllegalArgumentException("Unsupported user type " + user.getClass());
+ }
+
+ try {
+ Set<String> sysRoles = new HashSet<String>();
+ for (String role : rawAuthorization.getRoles()) {
+ User userOrGroup = (User) userAdminToUse.getRole(role);
+ Authorization auth = systemRoles.getAuthorization(userOrGroup);
+ systemRoles: for (String systemRole : auth.getRoles()) {
+ if (role.equals(systemRole))
+ continue systemRoles;
+ sysRoles.add(systemRole);
+ }
+// sysRoles.addAll(Arrays.asList(auth.getRoles()));
+ }
+ addAbstractSystemRoles(rawAuthorization, sysRoles);
+ Authorization authorization = new AggregatingAuthorization(usernameToUse, displayNameToUse, sysRoles,
+ rawAuthorization.getRoles());
+ return authorization;
+ } finally {
+ if (userAdminToUse != null && userAdminToUse.isScoped()) {
+ userAdminToUse.destroy();
+ }
+ }
+ }
+
+ /**
+ * Enrich with application-specific roles which are strictly programmatic, such
+ * as anonymous/user semantics.
+ */
+ protected void addAbstractSystemRoles(Authorization rawAuthorization, Set<String> sysRoles) {
+
+ }
+
+ //
+ // USER ADMIN AGGREGATOR
+ //
+ protected void addUserDirectory(AbstractUserDirectory userDirectory) {
+ LdapName baseDn = userDirectory.getBaseDn();
+ if (isSystemRolesBaseDn(baseDn)) {
+ this.systemRoles = userDirectory;
+ systemRoles.setExternalRoles(this);
+ } else if (isTokensBaseDn(baseDn)) {
+ this.tokens = userDirectory;
+ tokens.setExternalRoles(this);
+ } else {
+ if (businessRoles.containsKey(baseDn))
+ throw new UserDirectoryException("There is already a user admin for " + baseDn);
+ businessRoles.put(baseDn, userDirectory);
+ }
+ userDirectory.init();
+ postAdd(userDirectory);
+ }
+
+ /** Called after a new user directory has been added */
+ protected void postAdd(AbstractUserDirectory userDirectory) {
+ }
+
+// private UserAdmin findUserAdmin(User user) {
+// if (user == null)
+// throw new IllegalArgumentException("User should not be null");
+// AbstractUserDirectory userAdmin = findUserAdmin(user.getName());
+// if (user instanceof DirectoryUser) {
+// return userAdmin;
+// } else {
+// return userAdmin.scope(user);
+// }
+// }
+
+ private AbstractUserDirectory findUserAdmin(String name) {
+ try {
+ return findUserAdmin(new LdapName(name));
+ } catch (InvalidNameException e) {
+ throw new UserDirectoryException("Badly formatted name " + name, e);
+ }
+ }
+
+ private AbstractUserDirectory findUserAdmin(LdapName name) {
+ if (name.startsWith(systemRolesBaseDn))
+ return systemRoles;
+ if (tokensBaseDn != null && name.startsWith(tokensBaseDn))
+ return tokens;
+ List<AbstractUserDirectory> res = new ArrayList<>(1);
+ userDirectories: for (LdapName baseDn : businessRoles.keySet()) {
+ AbstractUserDirectory userDirectory = businessRoles.get(baseDn);
+ if (name.startsWith(baseDn)) {
+ if (userDirectory.isDisabled())
+ continue userDirectories;
+// if (res.isEmpty()) {
+ res.add(userDirectory);
+// } else {
+// for (AbstractUserDirectory ud : res) {
+// LdapName bd = ud.getBaseDn();
+// if (userDirectory.getBaseDn().startsWith(bd)) {
+// // child user directory
+// }
+// }
+// }
+ }
+ }
+ if (res.size() == 0)
+ throw new UserDirectoryException("Cannot find user admin for " + name);
+ if (res.size() > 1)
+ throw new UserDirectoryException("Multiple user admin found for " + name);
+ return res.get(0);
+ }
+
+ protected boolean isSystemRolesBaseDn(LdapName baseDn) {
+ return baseDn.equals(systemRolesBaseDn);
+ }
+
+ protected boolean isTokensBaseDn(LdapName baseDn) {
+ return tokensBaseDn != null && baseDn.equals(tokensBaseDn);
+ }
+
+// protected Dictionary<String, Object> currentState() {
+// Dictionary<String, Object> res = new Hashtable<String, Object>();
+// // res.put(NodeConstants.CN, NodeConstants.DEFAULT);
+// for (LdapName name : businessRoles.keySet()) {
+// AbstractUserDirectory userDirectory = businessRoles.get(name);
+// String uri = UserAdminConf.propertiesAsUri(userDirectory.getProperties()).toString();
+// res.put(uri, "");
+// }
+// return res;
+// }
+
+ public void destroy() {
+ for (LdapName name : businessRoles.keySet()) {
+ AbstractUserDirectory userDirectory = businessRoles.get(name);
+ destroy(userDirectory);
+ }
+ businessRoles.clear();
+ businessRoles = null;
+ destroy(systemRoles);
+ systemRoles = null;
+ }
+
+ private void destroy(AbstractUserDirectory userDirectory) {
+ preDestroy(userDirectory);
+ userDirectory.destroy();
+ }
+
+ protected void removeUserDirectory(LdapName baseDn) {
+ if (isSystemRolesBaseDn(baseDn))
+ throw new UserDirectoryException("System roles cannot be removed ");
+ if (!businessRoles.containsKey(baseDn))
+ throw new UserDirectoryException("No user directory registered for " + baseDn);
+ AbstractUserDirectory userDirectory = businessRoles.remove(baseDn);
+ destroy(userDirectory);
+ }
+
+ /**
+ * Called before each user directory is destroyed, so that additional actions
+ * can be performed.
+ */
+ protected void preDestroy(AbstractUserDirectory userDirectory) {
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import javax.naming.ldap.LdapName;
+
+import org.osgi.service.useradmin.User;
+
+/**
+ * A special user type used during authentication in order to provide the
+ * credentials required for scoping the user admin.
+ */
+public class AuthenticatingUser implements User {
+ /** From com.sun.security.auth.module.*LoginModule */
+ public final static String SHARED_STATE_NAME = "javax.security.auth.login.name";
+ /** From com.sun.security.auth.module.*LoginModule */
+ public final static String SHARED_STATE_PWD = "javax.security.auth.login.password";
+
+ private final String name;
+ private final Dictionary<String, Object> credentials;
+
+ public AuthenticatingUser(LdapName name) {
+ if (name == null)
+ throw new NullPointerException("Provided name cannot be null.");
+ this.name = name.toString();
+ this.credentials = new Hashtable<>();
+ }
+
+ public AuthenticatingUser(String name, Dictionary<String, Object> credentials) {
+ this.name = name;
+ this.credentials = credentials;
+ }
+
+ public AuthenticatingUser(String name, char[] password) {
+ if (name == null)
+ throw new NullPointerException("Provided name cannot be null.");
+ this.name = name;
+ credentials = new Hashtable<>();
+ credentials.put(SHARED_STATE_NAME, name);
+ byte[] pwd = DigestUtils.charsToBytes(password);
+ credentials.put(SHARED_STATE_PWD, pwd);
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public int getType() {
+ return User.USER;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Override
+ public Dictionary getProperties() {
+ throw new UnsupportedOperationException();
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Override
+ public Dictionary getCredentials() {
+ return credentials;
+ }
+
+ @Override
+ public boolean hasCredential(String key, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "Authenticating user " + name;
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.spec.KeySpec;
+import java.util.Arrays;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+/** Utilities around digests, mostly those related to passwords. */
+class DigestUtils {
+ final static String PASSWORD_SCHEME_SHA = "SHA";
+ final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256";
+
+ static byte[] sha1(byte[] bytes) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA1");
+ digest.update(bytes);
+ byte[] checksum = digest.digest();
+ return checksum;
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot SHA1 digest", e);
+ }
+ }
+
+ static byte[] toPasswordScheme(String passwordScheme, char[] password, byte[] salt, Integer iterations,
+ Integer keyLength) {
+ try {
+ if (PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
+ MessageDigest digest = MessageDigest.getInstance("SHA1");
+ byte[] bytes = charsToBytes(password);
+ digest.update(bytes);
+ return digest.digest();
+ } else if (PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) {
+ KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength);
+
+ SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+ final int ITERATION_LENGTH = 4;
+ byte[] key = f.generateSecret(spec).getEncoded();
+ byte[] result = new byte[ITERATION_LENGTH + salt.length + key.length];
+ byte iterationsArr[] = new BigInteger(iterations.toString()).toByteArray();
+ if (iterationsArr.length < ITERATION_LENGTH) {
+ Arrays.fill(result, 0, ITERATION_LENGTH - iterationsArr.length, (byte) 0);
+ System.arraycopy(iterationsArr, 0, result, ITERATION_LENGTH - iterationsArr.length,
+ iterationsArr.length);
+ } else {
+ System.arraycopy(iterationsArr, 0, result, 0, ITERATION_LENGTH);
+ }
+ System.arraycopy(salt, 0, result, ITERATION_LENGTH, salt.length);
+ System.arraycopy(key, 0, result, ITERATION_LENGTH + salt.length, key.length);
+ return result;
+ } else {
+ throw new UnsupportedOperationException("Unkown password scheme " + passwordScheme);
+ }
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot digest", e);
+ }
+ }
+
+ static char[] bytesToChars(Object obj) {
+ if (obj instanceof char[])
+ return (char[]) obj;
+ if (!(obj instanceof byte[]))
+ throw new IllegalArgumentException(obj.getClass() + " is not a byte array");
+ ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj);
+ CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer);
+ char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(), toBuffer.limit());
+ // Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data
+ // Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data
+ // Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data
+ return res;
+ }
+
+ static byte[] charsToBytes(char[] chars) {
+ CharBuffer charBuffer = CharBuffer.wrap(chars);
+ ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
+ byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
+ // Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data
+ // Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
+ return bytes;
+ }
+
+ static String sha1str(String str) {
+ byte[] hash = sha1(str.getBytes(StandardCharsets.UTF_8));
+ return encodeHexString(hash);
+ }
+
+ final private static char[] hexArray = "0123456789abcdef".toCharArray();
+
+ /**
+ * From
+ * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
+ * -a-hex-string-in-java
+ */
+ public static String encodeHexString(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+ private DigestUtils() {
+ }
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.util.List;
+
+import javax.naming.ldap.LdapName;
+
+import org.osgi.service.useradmin.Group;
+
+/** A group in a user directroy. */
+interface DirectoryGroup extends Group, DirectoryUser {
+ List<LdapName> getMemberNames();
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.osgi.service.useradmin.User;
+
+/** A user in a user directory. */
+interface DirectoryUser extends User {
+ LdapName getDn();
+
+ Attributes getAttributes();
+
+ void publishAttributes(Attributes modifiedAttributes);
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.naming.InvalidNameException;
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.DnsBrowser;
+import org.argeo.naming.LdapAttrs;
+
+/** Free IPA specific conventions. */
+public class IpaUtils {
+ public final static String IPA_USER_BASE = "cn=users,cn=accounts";
+ public final static String IPA_GROUP_BASE = "cn=groups,cn=accounts";
+ public final static String IPA_SERVICE_BASE = "cn=services,cn=accounts";
+
+ private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase();
+
+ public final static String IPA_USER_DIRECTORY_CONFIG = UserAdminConf.userBase + "=" + IPA_USER_BASE + "&"
+ + UserAdminConf.groupBase + "=" + IPA_GROUP_BASE + "&" + UserAdminConf.readOnly + "=true";
+
+ @Deprecated
+ static String domainToUserDirectoryConfigPath(String realm) {
+ return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + UserAdminConf.realm.name() + "=" + realm;
+ }
+
+ public static void addIpaConfig(String realm, Dictionary<String, Object> properties) {
+ properties.put(UserAdminConf.baseDn.name(), domainToBaseDn(realm));
+ properties.put(UserAdminConf.realm.name(), realm);
+ properties.put(UserAdminConf.userBase.name(), IPA_USER_BASE);
+ properties.put(UserAdminConf.groupBase.name(), IPA_GROUP_BASE);
+ properties.put(UserAdminConf.readOnly.name(), Boolean.TRUE.toString());
+ }
+
+ public static String domainToBaseDn(String domain) {
+ String[] dcs = domain.split("\\.");
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < dcs.length; i++) {
+ if (i != 0)
+ sb.append(',');
+ String dc = dcs[i];
+ sb.append(LdapAttrs.dc.name()).append('=').append(dc.toLowerCase());
+ }
+ return sb.toString();
+ }
+
+ public static LdapName kerberosToDn(String kerberosName) {
+ String[] kname = kerberosName.split("@");
+ String username = kname[0];
+ String baseDn = domainToBaseDn(kname[1]);
+ String dn;
+ if (!username.contains("/"))
+ dn = LdapAttrs.uid + "=" + username + "," + IPA_USER_BASE + "," + baseDn;
+ else
+ dn = KRB_PRINCIPAL_NAME + "=" + kerberosName + "," + IPA_SERVICE_BASE + "," + baseDn;
+ try {
+ return new LdapName(dn);
+ } catch (InvalidNameException e) {
+ throw new IllegalArgumentException("Badly formatted name for " + kerberosName + ": " + dn);
+ }
+ }
+
+ private IpaUtils() {
+
+ }
+
+ public static String kerberosDomainFromDns() {
+ String kerberosDomain;
+ try (DnsBrowser dnsBrowser = new DnsBrowser()) {
+ InetAddress localhost = InetAddress.getLocalHost();
+ String hostname = localhost.getHostName();
+ String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
+ kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
+ return kerberosDomain;
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot determine Kerberos domain from DNS", e);
+ }
+
+ }
+
+ public static Dictionary<String, Object> convertIpaUri(URI uri) {
+ String path = uri.getPath();
+ String kerberosRealm;
+ if (path == null || path.length() <= 1) {
+ kerberosRealm = kerberosDomainFromDns();
+ } else {
+ kerberosRealm = path.substring(1);
+ }
+
+ if (kerberosRealm == null)
+ throw new UserDirectoryException("No Kerberos domain available for " + uri);
+ // TODO intergrate CA certificate in truststore
+ // String schemeToUse = SCHEME_LDAPS;
+ String schemeToUse = UserAdminConf.SCHEME_LDAP;
+ List<String> ldapHosts;
+ String ldapHostsStr = uri.getHost();
+ if (ldapHostsStr == null || ldapHostsStr.trim().equals("")) {
+ try (DnsBrowser dnsBrowser = new DnsBrowser()) {
+ ldapHosts = dnsBrowser.getSrvRecordsAsHosts("_ldap._tcp." + kerberosRealm.toLowerCase(),
+ schemeToUse.equals(UserAdminConf.SCHEME_LDAP) ? true : false);
+ if (ldapHosts == null || ldapHosts.size() == 0) {
+ throw new UserDirectoryException("Cannot configure LDAP for IPA " + uri);
+ } else {
+ ldapHostsStr = ldapHosts.get(0);
+ }
+ } catch (NamingException | IOException e) {
+ throw new UserDirectoryException("cannot convert IPA uri " + uri, e);
+ }
+ } else {
+ ldapHosts = new ArrayList<>();
+ ldapHosts.add(ldapHostsStr);
+ }
+
+ StringBuilder uriStr = new StringBuilder();
+ try {
+ for (String host : ldapHosts) {
+ URI convertedUri = new URI(schemeToUse + "://" + host + "/");
+ uriStr.append(convertedUri).append(' ');
+ }
+ } catch (URISyntaxException e) {
+ throw new UserDirectoryException("cannot convert IPA uri " + uri, e);
+ }
+
+ Hashtable<String, Object> res = new Hashtable<>();
+ res.put(UserAdminConf.uri.name(), uriStr.toString());
+ addIpaConfig(kerberosRealm, res);
+ return res;
+ }
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import javax.naming.CommunicationException;
+import javax.naming.Context;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.LdapAttrs;
+
+/** A synchronized wrapper for a single {@link InitialLdapContext}. */
+// TODO implement multiple contexts and connection pooling.
+class LdapConnection {
+ private InitialLdapContext initialLdapContext = null;
+
+ LdapConnection(String url, Dictionary<String, ?> properties) {
+ try {
+ Hashtable<String, Object> connEnv = new Hashtable<String, Object>();
+ connEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+ connEnv.put(Context.PROVIDER_URL, url);
+ connEnv.put("java.naming.ldap.attributes.binary", LdapAttrs.userPassword.name());
+ // use pooling in order to avoid connection timeout
+// connEnv.put("com.sun.jndi.ldap.connect.pool", "true");
+// connEnv.put("com.sun.jndi.ldap.connect.pool.timeout", 300000);
+
+ initialLdapContext = new InitialLdapContext(connEnv, null);
+ // StartTlsResponse tls = (StartTlsResponse) ctx
+ // .extendedOperation(new StartTlsRequest());
+ // tls.negotiate();
+ Object securityAuthentication = properties.get(Context.SECURITY_AUTHENTICATION);
+ if (securityAuthentication != null)
+ initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, securityAuthentication);
+ else
+ initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
+ Object principal = properties.get(Context.SECURITY_PRINCIPAL);
+ if (principal != null) {
+ initialLdapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, principal.toString());
+ Object creds = properties.get(Context.SECURITY_CREDENTIALS);
+ if (creds != null) {
+ initialLdapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, creds.toString());
+ }
+ }
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot connect to LDAP", e);
+ }
+
+ }
+
+ public void init() {
+
+ }
+
+ public void destroy() {
+ try {
+ // tls.close();
+ initialLdapContext.close();
+ initialLdapContext = null;
+ } catch (NamingException e) {
+ e.printStackTrace();
+ }
+ }
+
+ protected InitialLdapContext getLdapContext() {
+ return initialLdapContext;
+ }
+
+ protected void reconnect() throws NamingException {
+ initialLdapContext.reconnect(initialLdapContext.getConnectControls());
+ }
+
+ public synchronized NamingEnumeration<SearchResult> search(LdapName searchBase, String searchFilter,
+ SearchControls searchControls) throws NamingException {
+ NamingEnumeration<SearchResult> results;
+ try {
+ results = getLdapContext().search(searchBase, searchFilter, searchControls);
+ } catch (CommunicationException e) {
+ reconnect();
+ results = getLdapContext().search(searchBase, searchFilter, searchControls);
+ }
+ return results;
+ }
+
+ public synchronized Attributes getAttributes(LdapName name) throws NamingException {
+ try {
+ return getLdapContext().getAttributes(name);
+ } catch (CommunicationException e) {
+ reconnect();
+ return getLdapContext().getAttributes(name);
+ }
+ }
+
+ synchronized void prepareChanges(UserDirectoryWorkingCopy wc) throws NamingException {
+ // make sure connection will work
+ reconnect();
+
+ // delete
+ for (LdapName dn : wc.getDeletedUsers().keySet()) {
+ if (!entryExists(dn))
+ throw new UserDirectoryException("User to delete no found " + dn);
+ }
+ // add
+ for (LdapName dn : wc.getNewUsers().keySet()) {
+ if (entryExists(dn))
+ throw new UserDirectoryException("User to create found " + dn);
+ }
+ // modify
+ for (LdapName dn : wc.getModifiedUsers().keySet()) {
+ if (!wc.getNewUsers().containsKey(dn) && !entryExists(dn))
+ throw new UserDirectoryException("User to modify not found " + dn);
+ }
+
+ }
+
+ protected boolean entryExists(LdapName dn) throws NamingException {
+ try {
+ return getAttributes(dn).size() != 0;
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ synchronized void commitChanges(UserDirectoryWorkingCopy wc) throws NamingException {
+ // delete
+ for (LdapName dn : wc.getDeletedUsers().keySet()) {
+ getLdapContext().destroySubcontext(dn);
+ }
+ // add
+ for (LdapName dn : wc.getNewUsers().keySet()) {
+ DirectoryUser user = wc.getNewUsers().get(dn);
+ getLdapContext().createSubcontext(dn, user.getAttributes());
+ }
+ // modify
+ for (LdapName dn : wc.getModifiedUsers().keySet()) {
+ Attributes modifiedAttrs = wc.getModifiedUsers().get(dn);
+ getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs);
+ }
+ }
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import static org.argeo.naming.LdapAttrs.objectClass;
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.List;
+
+import javax.naming.AuthenticationNotSupportedException;
+import javax.naming.Binding;
+import javax.naming.Context;
+import javax.naming.InvalidNameException;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.LdapName;
+
+import org.osgi.framework.Filter;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/** A user admin based on a LDAP server. */
+public class LdapUserAdmin extends AbstractUserDirectory {
+ private LdapConnection ldapConnection;
+
+ public LdapUserAdmin(Dictionary<String, ?> properties) {
+ this(properties, false);
+ }
+
+ public LdapUserAdmin(Dictionary<String, ?> properties, boolean scoped) {
+ super(null, properties, scoped);
+ ldapConnection = new LdapConnection(getUri().toString(), properties);
+ }
+
+ public void destroy() {
+ ldapConnection.destroy();
+ }
+
+ @Override
+ protected AbstractUserDirectory scope(User user) {
+ Dictionary<String, Object> credentials = user.getCredentials();
+ String username = (String) credentials.get(SHARED_STATE_USERNAME);
+ if (username == null)
+ username = user.getName();
+ Dictionary<String, Object> properties = cloneProperties();
+ properties.put(Context.SECURITY_PRINCIPAL, username.toString());
+ Object pwdCred = credentials.get(SHARED_STATE_PASSWORD);
+ byte[] pwd = (byte[]) pwdCred;
+ if (pwd != null) {
+ char[] password = DigestUtils.bytesToChars(pwd);
+ properties.put(Context.SECURITY_CREDENTIALS, new String(password));
+ } else {
+ properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
+ }
+ return new LdapUserAdmin(properties, true);
+ }
+
+// protected InitialLdapContext getLdapContext() {
+// return initialLdapContext;
+// }
+
+ @Override
+ protected Boolean daoHasRole(LdapName dn) {
+ try {
+ return daoGetRole(dn) != null;
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ @Override
+ protected DirectoryUser daoGetRole(LdapName name) throws NameNotFoundException {
+ try {
+ Attributes attrs = ldapConnection.getAttributes(name);
+ if (attrs.size() == 0)
+ return null;
+ int roleType = roleType(name);
+ LdifUser res;
+ if (roleType == Role.GROUP)
+ res = new LdifGroup(this, name, attrs);
+ else if (roleType == Role.USER)
+ res = new LdifUser(this, name, attrs);
+ else
+ throw new UserDirectoryException("Unsupported LDAP type for " + name);
+ return res;
+ } catch (NameNotFoundException e) {
+ throw e;
+ } catch (NamingException e) {
+ return null;
+ }
+ }
+
+ @Override
+ protected List<DirectoryUser> doGetRoles(Filter f) {
+ ArrayList<DirectoryUser> res = new ArrayList<DirectoryUser>();
+ try {
+ String searchFilter = f != null ? f.toString()
+ : "(|(" + objectClass + "=" + getUserObjectClass() + ")(" + objectClass + "="
+ + getGroupObjectClass() + "))";
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+
+ LdapName searchBase = getBaseDn();
+ NamingEnumeration<SearchResult> results = ldapConnection.search(searchBase, searchFilter, searchControls);
+
+ results: while (results.hasMoreElements()) {
+ SearchResult searchResult = results.next();
+ Attributes attrs = searchResult.getAttributes();
+ Attribute objectClassAttr = attrs.get(objectClass.name());
+ LdapName dn = toDn(searchBase, searchResult);
+ LdifUser role;
+ if (objectClassAttr.contains(getGroupObjectClass())
+ || objectClassAttr.contains(getGroupObjectClass().toLowerCase()))
+ role = new LdifGroup(this, dn, attrs);
+ else if (objectClassAttr.contains(getUserObjectClass())
+ || objectClassAttr.contains(getUserObjectClass().toLowerCase()))
+ role = new LdifUser(this, dn, attrs);
+ else {
+// log.warn("Unsupported LDAP type for " + searchResult.getName());
+ continue results;
+ }
+ res.add(role);
+ }
+ return res;
+ } catch (AuthenticationNotSupportedException e) {
+ // ignore (typically an unsupported anonymous bind)
+ // TODO better logging
+ return res;
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new UserDirectoryException("Cannot get roles for filter " + f, e);
+ }
+ }
+
+ private LdapName toDn(LdapName baseDn, Binding binding) throws InvalidNameException {
+ return new LdapName(binding.isRelative() ? binding.getName() + "," + baseDn : binding.getName());
+ }
+
+ @Override
+ protected List<LdapName> getDirectGroups(LdapName dn) {
+ List<LdapName> directGroups = new ArrayList<LdapName>();
+ try {
+ String searchFilter = "(&(" + objectClass + "=" + getGroupObjectClass() + ")(" + getMemberAttributeId()
+ + "=" + dn + "))";
+
+ SearchControls searchControls = new SearchControls();
+ searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+
+ LdapName searchBase = getBaseDn();
+ NamingEnumeration<SearchResult> results = ldapConnection.search(searchBase, searchFilter, searchControls);
+
+ while (results.hasMoreElements()) {
+ SearchResult searchResult = (SearchResult) results.nextElement();
+ directGroups.add(toDn(searchBase, searchResult));
+ }
+ return directGroups;
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot populate direct members of " + dn, e);
+ }
+ }
+
+ @Override
+ protected void prepare(UserDirectoryWorkingCopy wc) {
+ try {
+ ldapConnection.prepareChanges(wc);
+ } catch (NamingException e) {
+ throw new UserDirectoryException("Cannot prepare LDAP", e);
+ }
+ }
+
+ @Override
+ protected void commit(UserDirectoryWorkingCopy wc) {
+ try {
+ ldapConnection.commitChanges(wc);
+ } catch (NamingException e) {
+ throw new UserDirectoryException("Cannot commit LDAP", e);
+ }
+ }
+
+ @Override
+ protected void rollback(UserDirectoryWorkingCopy wc) {
+ // prepare not impacting
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.List;
+
+import org.argeo.naming.LdapAttrs;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/** Basic authorization. */
+class LdifAuthorization implements Authorization {
+ private final String name;
+ private final String displayName;
+ private final List<String> allRoles;
+
+ public LdifAuthorization(User user, List<Role> allRoles) {
+ if (user == null) {
+ this.name = null;
+ this.displayName = "anonymous";
+ } else {
+ this.name = user.getName();
+ this.displayName = extractDisplayName(user);
+ }
+ // roles
+ String[] roles = new String[allRoles.size()];
+ for (int i = 0; i < allRoles.size(); i++) {
+ roles[i] = allRoles.get(i).getName();
+ }
+ this.allRoles = Collections.unmodifiableList(Arrays.asList(roles));
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean hasRole(String name) {
+ return allRoles.contains(name);
+ }
+
+ @Override
+ public String[] getRoles() {
+ return allRoles.toArray(new String[allRoles.size()]);
+ }
+
+ @Override
+ public int hashCode() {
+ if (name == null)
+ return super.hashCode();
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Authorization))
+ return false;
+ Authorization that = (Authorization) obj;
+ if (name == null)
+ return that.getName() == null;
+ return name.equals(that.getName());
+ }
+
+ @Override
+ public String toString() {
+ return displayName;
+ }
+
+ final static String extractDisplayName(User user) {
+ Dictionary<String, Object> props = user.getProperties();
+ Object displayName = props.get(LdapAttrs.displayName);
+ if (displayName == null)
+ displayName = props.get(LdapAttrs.cn);
+ if (displayName == null)
+ displayName = props.get(LdapAttrs.uid);
+ if (displayName == null)
+ displayName = user.getName();
+ if (displayName == null)
+ throw new UserDirectoryException("Cannot set display name for " + user);
+ return displayName.toString();
+ }
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.naming.InvalidNameException;
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.osgi.service.useradmin.Role;
+
+/** Directory group implementation */
+class LdifGroup extends LdifUser implements DirectoryGroup {
+ private final String memberAttributeId;
+
+ LdifGroup(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) {
+ super(userAdmin, dn, attributes);
+ memberAttributeId = userAdmin.getMemberAttributeId();
+ }
+
+ @Override
+ public boolean addMember(Role role) {
+ try {
+ Role foundRole = findRole(new LdapName(role.getName()));
+ if (foundRole == null)
+ throw new UnsupportedOperationException(
+ "Adding role " + role.getName() + " is unsupported within this context.");
+ } catch (InvalidNameException e) {
+ throw new IllegalArgumentException("Role name" + role.getName() + " is badly formatted");
+ }
+
+ getUserAdmin().checkEdit();
+ if (!isEditing())
+ startEditing();
+
+ Attribute member = getAttributes().get(memberAttributeId);
+ if (member != null) {
+ if (member.contains(role.getName()))
+ return false;
+ else
+ member.add(role.getName());
+ } else
+ getAttributes().put(memberAttributeId, role.getName());
+ return true;
+ }
+
+ @Override
+ public boolean addRequiredMember(Role role) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeMember(Role role) {
+ getUserAdmin().checkEdit();
+ if (!isEditing())
+ startEditing();
+
+ Attribute member = getAttributes().get(memberAttributeId);
+ if (member != null) {
+ if (!member.contains(role.getName()))
+ return false;
+ member.remove(role.getName());
+ return true;
+ } else
+ return false;
+ }
+
+ @Override
+ public Role[] getMembers() {
+ List<Role> directMembers = new ArrayList<Role>();
+ for (LdapName ldapName : getMemberNames()) {
+ Role role = findRole(ldapName);
+ if (role == null) {
+ throw new UserDirectoryException("Role " + ldapName + " cannot be added.");
+ }
+ directMembers.add(role);
+ }
+ return directMembers.toArray(new Role[directMembers.size()]);
+ }
+
+ /**
+ * Whether a role with this name can be found from this context.
+ *
+ * @return The related {@link Role} or <code>null</code>.
+ */
+ protected Role findRole(LdapName ldapName) {
+ Role role = getUserAdmin().getRole(ldapName.toString());
+ if (role == null) {
+ if (getUserAdmin().getExternalRoles() != null)
+ role = getUserAdmin().getExternalRoles().getRole(ldapName.toString());
+ }
+ return role;
+ }
+
+ @Override
+ public List<LdapName> getMemberNames() {
+ Attribute memberAttribute = getAttributes().get(memberAttributeId);
+ if (memberAttribute == null)
+ return new ArrayList<LdapName>();
+ try {
+ List<LdapName> roles = new ArrayList<LdapName>();
+ NamingEnumeration<?> values = memberAttribute.getAll();
+ while (values.hasMore()) {
+ LdapName dn = new LdapName(values.next().toString());
+ roles.add(dn);
+ }
+ return roles;
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot get members", e);
+ }
+ }
+
+ @Override
+ public Role[] getRequiredMembers() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int getType() {
+ return GROUP;
+ }
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.AuthPassword;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.SharedSecret;
+
+/** Directory user implementation */
+class LdifUser implements DirectoryUser {
+ private final AbstractUserDirectory userAdmin;
+
+ private final LdapName dn;
+
+ private final boolean frozen;
+ private Attributes publishedAttributes;
+
+ private final AttributeDictionary properties;
+ private final AttributeDictionary credentials;
+
+ LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) {
+ this(userAdmin, dn, attributes, false);
+ }
+
+ private LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes, boolean frozen) {
+ this.userAdmin = userAdmin;
+ this.dn = dn;
+ this.publishedAttributes = attributes;
+ properties = new AttributeDictionary(false);
+ credentials = new AttributeDictionary(true);
+ this.frozen = frozen;
+ }
+
+ @Override
+ public String getName() {
+ return dn.toString();
+ }
+
+ @Override
+ public int getType() {
+ return USER;
+ }
+
+ @Override
+ public Dictionary<String, Object> getProperties() {
+ return properties;
+ }
+
+ @Override
+ public Dictionary<String, Object> getCredentials() {
+ return credentials;
+ }
+
+ @Override
+ public boolean hasCredential(String key, Object value) {
+ if (key == null) {
+ // TODO check other sources (like PKCS12)
+ // String pwd = new String((char[]) value);
+ // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112)
+ char[] password = DigestUtils.bytesToChars(value);
+ AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password);
+ if (authPassword != null) {
+ if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) {
+ SharedSecret onceToken = new SharedSecret(authPassword);
+ if (onceToken.isExpired()) {
+ // AuthPassword.remove(getAttributes(), onceToken);
+ return false;
+ } else {
+ // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
+ return true;
+ }
+ // TODO delete expired tokens?
+ } else {
+ // TODO implement SHA
+ throw new UnsupportedOperationException(
+ "Unsupported authPassword scheme " + authPassword.getAuthScheme());
+ }
+ }
+
+ // Regular password
+// byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256);
+ if (hasCredential(LdapAttrs.userPassword.name(), DigestUtils.charsToBytes(password)))
+ return true;
+ return false;
+ }
+
+ Object storedValue = getCredentials().get(key);
+ if (storedValue == null || value == null)
+ return false;
+ if (!(value instanceof String || value instanceof byte[]))
+ return false;
+ if (storedValue instanceof String && value instanceof String)
+ return storedValue.equals(value);
+ if (storedValue instanceof byte[] && value instanceof byte[]) {
+ String storedBase64 = new String((byte[]) storedValue, US_ASCII);
+ String passwordScheme = null;
+ if (storedBase64.charAt(0) == '{') {
+ int index = storedBase64.indexOf('}');
+ if (index > 0) {
+ passwordScheme = storedBase64.substring(1, index);
+ String storedValueBase64 = storedBase64.substring(index + 1);
+ byte[] storedValueBytes = Base64.getDecoder().decode(storedValueBase64);
+ char[] passwordValue = DigestUtils.bytesToChars((byte[]) value);
+ byte[] valueBytes;
+ if (DigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
+ valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, null);
+ } else if (DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) {
+ // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/
+ byte[] iterationsArr = Arrays.copyOfRange(storedValueBytes, 0, 4);
+ BigInteger iterations = new BigInteger(iterationsArr);
+ byte[] salt = Arrays.copyOfRange(storedValueBytes, iterationsArr.length,
+ iterationsArr.length + 64);
+ byte[] keyArr = Arrays.copyOfRange(storedValueBytes, iterationsArr.length + salt.length,
+ storedValueBytes.length);
+ int keyLengthBits = keyArr.length * 8;
+ valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt,
+ iterations.intValue(), keyLengthBits);
+ } else {
+ throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme);
+ }
+ return Arrays.equals(storedValueBytes, valueBytes);
+ }
+ }
+ }
+// if (storedValue instanceof byte[] && value instanceof byte[]) {
+// return Arrays.equals((byte[]) storedValue, (byte[]) value);
+// }
+ return false;
+ }
+
+ /** Hash the password */
+ byte[] sha1hash(char[] password) {
+ byte[] hashedPassword = ("{SHA}"
+ + Base64.getEncoder().encodeToString(DigestUtils.sha1(DigestUtils.charsToBytes(password))))
+ .getBytes(StandardCharsets.UTF_8);
+ return hashedPassword;
+ }
+
+// byte[] hash(char[] password, String passwordScheme) {
+// if (passwordScheme == null)
+// passwordScheme = DigestUtils.PASSWORD_SCHEME_SHA;
+// byte[] hashedPassword = ("{" + passwordScheme + "}"
+// + Base64.getEncoder().encodeToString(DigestUtils.toPasswordScheme(passwordScheme, password)))
+// .getBytes(US_ASCII);
+// return hashedPassword;
+// }
+
+ @Override
+ public LdapName getDn() {
+ return dn;
+ }
+
+ @Override
+ public synchronized Attributes getAttributes() {
+ return isEditing() ? getModifiedAttributes() : publishedAttributes;
+ }
+
+ /** Should only be called from working copy thread. */
+ private synchronized Attributes getModifiedAttributes() {
+ assert getWc() != null;
+ return getWc().getAttributes(getDn());
+ }
+
+ protected synchronized boolean isEditing() {
+ return getWc() != null && getModifiedAttributes() != null;
+ }
+
+ private synchronized UserDirectoryWorkingCopy getWc() {
+ return userAdmin.getWorkingCopy();
+ }
+
+ protected synchronized void startEditing() {
+ if (frozen)
+ throw new UserDirectoryException("Cannot edit frozen view");
+ if (getUserAdmin().isReadOnly())
+ throw new UserDirectoryException("User directory is read-only");
+ assert getModifiedAttributes() == null;
+ getWc().startEditing(this);
+ // modifiedAttributes = (Attributes) publishedAttributes.clone();
+ }
+
+ public synchronized void publishAttributes(Attributes modifiedAttributes) {
+ publishedAttributes = modifiedAttributes;
+ }
+
+ public DirectoryUser getPublished() {
+ return new LdifUser(userAdmin, dn, publishedAttributes, true);
+ }
+
+ @Override
+ public int hashCode() {
+ return dn.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj instanceof LdifUser) {
+ LdifUser that = (LdifUser) obj;
+ return this.dn.equals(that.dn);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return dn.toString();
+ }
+
+ protected AbstractUserDirectory getUserAdmin() {
+ return userAdmin;
+ }
+
+ private class AttributeDictionary extends Dictionary<String, Object> {
+ private final List<String> effectiveKeys = new ArrayList<String>();
+ private final List<String> attrFilter;
+ private final Boolean includeFilter;
+
+ public AttributeDictionary(Boolean includeFilter) {
+ this.attrFilter = userAdmin.getCredentialAttributeIds();
+ this.includeFilter = includeFilter;
+ try {
+ NamingEnumeration<String> ids = getAttributes().getIDs();
+ while (ids.hasMore()) {
+ String id = ids.next();
+ if (includeFilter && attrFilter.contains(id))
+ effectiveKeys.add(id);
+ else if (!includeFilter && !attrFilter.contains(id))
+ effectiveKeys.add(id);
+ }
+ } catch (NamingException e) {
+ throw new UserDirectoryException("Cannot initialise attribute dictionary", e);
+ }
+ }
+
+ @Override
+ public int size() {
+ return effectiveKeys.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return effectiveKeys.size() == 0;
+ }
+
+ @Override
+ public Enumeration<String> keys() {
+ return Collections.enumeration(effectiveKeys);
+ }
+
+ @Override
+ public Enumeration<Object> elements() {
+ final Iterator<String> it = effectiveKeys.iterator();
+ return new Enumeration<Object>() {
+
+ @Override
+ public boolean hasMoreElements() {
+ return it.hasNext();
+ }
+
+ @Override
+ public Object nextElement() {
+ String key = it.next();
+ return get(key);
+ }
+
+ };
+ }
+
+ @Override
+ public Object get(Object key) {
+ try {
+ Attribute attr = getAttributes().get(key.toString());
+ if (attr == null)
+ return null;
+ Object value = attr.get();
+ if (value instanceof byte[]) {
+ if (key.equals(LdapAttrs.userPassword.name()))
+ // TODO other cases (certificates, images)
+ return value;
+ value = new String((byte[]) value, StandardCharsets.UTF_8);
+ }
+ if (attr.size() == 1)
+ return value;
+ if (!attr.getID().equals(LdapAttrs.objectClass.name()))
+ return value;
+ // special case for object class
+ NamingEnumeration<?> en = attr.getAll();
+ Set<String> objectClasses = new HashSet<String>();
+ while (en.hasMore()) {
+ String objectClass = en.next().toString();
+ objectClasses.add(objectClass);
+ }
+
+ if (objectClasses.contains(userAdmin.getUserObjectClass()))
+ return userAdmin.getUserObjectClass();
+ else if (objectClasses.contains(userAdmin.getGroupObjectClass()))
+ return userAdmin.getGroupObjectClass();
+ else
+ return value;
+ } catch (NamingException e) {
+ throw new UserDirectoryException("Cannot get value for attribute " + key, e);
+ }
+ }
+
+ @Override
+ public Object put(String key, Object value) {
+ if (key == null) {
+ // TODO persist to other sources (like PKCS12)
+ char[] password = DigestUtils.bytesToChars(value);
+ byte[] hashedPassword = sha1hash(password);
+ return put(LdapAttrs.userPassword.name(), hashedPassword);
+ }
+ if (key.startsWith("X-")) {
+ return put(LdapAttrs.authPassword.name(), value);
+ }
+
+ userAdmin.checkEdit();
+ if (!isEditing())
+ startEditing();
+
+ if (!(value instanceof String || value instanceof byte[]))
+ throw new IllegalArgumentException("Value must be String or byte[]");
+
+ if (includeFilter && !attrFilter.contains(key))
+ throw new IllegalArgumentException("Key " + key + " not included");
+ else if (!includeFilter && attrFilter.contains(key))
+ throw new IllegalArgumentException("Key " + key + " excluded");
+
+ try {
+ Attribute attribute = getModifiedAttributes().get(key.toString());
+ // if (attribute == null) // block unit tests
+ attribute = new BasicAttribute(key.toString());
+ if (value instanceof String && !isAsciiPrintable(((String) value)))
+ attribute.add(((String) value).getBytes(StandardCharsets.UTF_8));
+ else
+ attribute.add(value);
+ Attribute previousAttribute = getModifiedAttributes().put(attribute);
+ if (previousAttribute != null)
+ return previousAttribute.get();
+ else
+ return null;
+ } catch (NamingException e) {
+ throw new UserDirectoryException("Cannot get value for attribute " + key, e);
+ }
+ }
+
+ @Override
+ public Object remove(Object key) {
+ userAdmin.checkEdit();
+ if (!isEditing())
+ startEditing();
+
+ if (includeFilter && !attrFilter.contains(key))
+ throw new IllegalArgumentException("Key " + key + " not included");
+ else if (!includeFilter && attrFilter.contains(key))
+ throw new IllegalArgumentException("Key " + key + " excluded");
+
+ try {
+ Attribute attr = getModifiedAttributes().remove(key.toString());
+ if (attr != null)
+ return attr.get();
+ else
+ return null;
+ } catch (NamingException e) {
+ throw new UserDirectoryException("Cannot remove attribute " + key, e);
+ }
+ }
+ }
+
+ private static boolean isAsciiPrintable(String str) {
+ if (str == null) {
+ return false;
+ }
+ int sz = str.length();
+ for (int i = 0; i < sz; i++) {
+ if (isAsciiPrintable(str.charAt(i)) == false) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static boolean isAsciiPrintable(char ch) {
+ return ch >= 32 && ch < 127;
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import static org.argeo.naming.LdapAttrs.objectClass;
+import static org.argeo.naming.LdapObjs.inetOrgPerson;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.LdifParser;
+import org.argeo.naming.LdifWriter;
+import org.osgi.framework.Filter;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/** A user admin based on a LDIF files. */
+public class LdifUserAdmin extends AbstractUserDirectory {
+ private SortedMap<LdapName, DirectoryUser> users = new TreeMap<LdapName, DirectoryUser>();
+ private SortedMap<LdapName, DirectoryGroup> groups = new TreeMap<LdapName, DirectoryGroup>();
+
+ public LdifUserAdmin(String uri, String baseDn) {
+ this(fromUri(uri, baseDn), false);
+ }
+
+ public LdifUserAdmin(Dictionary<String, ?> properties) {
+ this(properties, false);
+ }
+
+ protected LdifUserAdmin(Dictionary<String, ?> properties, boolean scoped) {
+ super(null, properties, scoped);
+ }
+
+ public LdifUserAdmin(URI uri, Dictionary<String, ?> properties) {
+ super(uri, properties, false);
+ }
+
+ @Override
+ protected AbstractUserDirectory scope(User user) {
+ Dictionary<String, Object> credentials = user.getCredentials();
+ String username = (String) credentials.get(SHARED_STATE_USERNAME);
+ if (username == null)
+ username = user.getName();
+ Object pwdCred = credentials.get(SHARED_STATE_PASSWORD);
+ byte[] pwd = (byte[]) pwdCred;
+ if (pwd != null) {
+ char[] password = DigestUtils.bytesToChars(pwd);
+ User directoryUser = (User) getRole(username);
+ if (!directoryUser.hasCredential(null, password))
+ throw new UserDirectoryException("Invalid credentials");
+ } else {
+ throw new UserDirectoryException("Password is required");
+ }
+ Dictionary<String, Object> properties = cloneProperties();
+ properties.put(UserAdminConf.readOnly.name(), "true");
+ LdifUserAdmin scopedUserAdmin = new LdifUserAdmin(properties, true);
+ scopedUserAdmin.groups = Collections.unmodifiableSortedMap(groups);
+ scopedUserAdmin.users = Collections.unmodifiableSortedMap(users);
+ return scopedUserAdmin;
+ }
+
+ private static Dictionary<String, Object> fromUri(String uri, String baseDn) {
+ Hashtable<String, Object> res = new Hashtable<String, Object>();
+ res.put(UserAdminConf.uri.name(), uri);
+ res.put(UserAdminConf.baseDn.name(), baseDn);
+ return res;
+ }
+
+ public void init() {
+
+ try {
+ URI u = new URI(getUri());
+ if (u.getScheme().equals("file")) {
+ File file = new File(u);
+ if (!file.exists())
+ return;
+ }
+ load(u.toURL().openStream());
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot open URL " + getUri(), e);
+ }
+ }
+
+ public void save() {
+ if (getUri() == null)
+ throw new UserDirectoryException("Cannot save LDIF user admin: no URI is set");
+ if (isReadOnly())
+ throw new UserDirectoryException("Cannot save LDIF user admin: " + getUri() + " is read-only");
+ try (FileOutputStream out = new FileOutputStream(new File(new URI(getUri())))) {
+ save(out);
+ } catch (IOException | URISyntaxException e) {
+ throw new UserDirectoryException("Cannot save user admin to " + getUri(), e);
+ }
+ }
+
+ public void save(OutputStream out) throws IOException {
+ try {
+ LdifWriter ldifWriter = new LdifWriter(out);
+ for (LdapName name : groups.keySet())
+ ldifWriter.writeEntry(name, groups.get(name).getAttributes());
+ for (LdapName name : users.keySet())
+ ldifWriter.writeEntry(name, users.get(name).getAttributes());
+ } finally {
+ out.close();
+ }
+ }
+
+ protected void load(InputStream in) {
+ try {
+ users.clear();
+ groups.clear();
+
+ LdifParser ldifParser = new LdifParser();
+ SortedMap<LdapName, Attributes> allEntries = ldifParser.read(in);
+ for (LdapName key : allEntries.keySet()) {
+ Attributes attributes = allEntries.get(key);
+ // check for inconsistency
+ Set<String> lowerCase = new HashSet<String>();
+ NamingEnumeration<String> ids = attributes.getIDs();
+ while (ids.hasMoreElements()) {
+ String id = ids.nextElement().toLowerCase();
+ if (lowerCase.contains(id))
+ throw new UserDirectoryException(key + " has duplicate id " + id);
+ lowerCase.add(id);
+ }
+
+ // analyse object classes
+ NamingEnumeration<?> objectClasses = attributes.get(objectClass.name()).getAll();
+ // System.out.println(key);
+ objectClasses: while (objectClasses.hasMore()) {
+ String objectClass = objectClasses.next().toString();
+ // System.out.println(" " + objectClass);
+ if (objectClass.toLowerCase().equals(inetOrgPerson.name().toLowerCase())) {
+ users.put(key, new LdifUser(this, key, attributes));
+ break objectClasses;
+ } else if (objectClass.toLowerCase().equals(getGroupObjectClass().toLowerCase())) {
+ groups.put(key, new LdifGroup(this, key, attributes));
+ break objectClasses;
+ }
+ }
+ }
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot load user admin service from LDIF", e);
+ }
+ }
+
+ public void destroy() {
+ if (users == null || groups == null)
+ throw new UserDirectoryException("User directory " + getBaseDn() + " is already destroyed");
+ users = null;
+ groups = null;
+ }
+
+ @Override
+ protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException {
+ if (groups.containsKey(key))
+ return groups.get(key);
+ if (users.containsKey(key))
+ return users.get(key);
+ throw new NameNotFoundException(key + " not persisted");
+ }
+
+ @Override
+ protected Boolean daoHasRole(LdapName dn) {
+ return users.containsKey(dn) || groups.containsKey(dn);
+ }
+
+ protected List<DirectoryUser> doGetRoles(Filter f) {
+ ArrayList<DirectoryUser> res = new ArrayList<DirectoryUser>();
+ if (f == null) {
+ res.addAll(users.values());
+ res.addAll(groups.values());
+ } else {
+ for (DirectoryUser user : users.values()) {
+ if (f.match(user.getProperties()))
+ res.add(user);
+ }
+ for (DirectoryUser group : groups.values())
+ if (f.match(group.getProperties()))
+ res.add(group);
+ }
+ return res;
+ }
+
+ @Override
+ protected List<LdapName> getDirectGroups(LdapName dn) {
+ List<LdapName> directGroups = new ArrayList<LdapName>();
+ for (LdapName name : groups.keySet()) {
+ DirectoryGroup group = groups.get(name);
+ if (group.getMemberNames().contains(dn))
+ directGroups.add(group.getDn());
+ }
+ return directGroups;
+ }
+
+ @Override
+ protected void prepare(UserDirectoryWorkingCopy wc) {
+ // delete
+ for (LdapName dn : wc.getDeletedUsers().keySet()) {
+ if (users.containsKey(dn))
+ users.remove(dn);
+ else if (groups.containsKey(dn))
+ groups.remove(dn);
+ else
+ throw new UserDirectoryException("User to delete not found " + dn);
+ }
+ // add
+ for (LdapName dn : wc.getNewUsers().keySet()) {
+ DirectoryUser user = wc.getNewUsers().get(dn);
+ if (users.containsKey(dn) || groups.containsKey(dn))
+ throw new UserDirectoryException("User to create found " + dn);
+ else if (Role.USER == user.getType())
+ users.put(dn, user);
+ else if (Role.GROUP == user.getType())
+ groups.put(dn, (DirectoryGroup) user);
+ else
+ throw new UserDirectoryException("Unsupported role type " + user.getType() + " for new user " + dn);
+ }
+ // modify
+ for (LdapName dn : wc.getModifiedUsers().keySet()) {
+ Attributes modifiedAttrs = wc.getModifiedUsers().get(dn);
+ DirectoryUser user;
+ if (users.containsKey(dn))
+ user = users.get(dn);
+ else if (groups.containsKey(dn))
+ user = groups.get(dn);
+ else
+ throw new UserDirectoryException("User to modify no found " + dn);
+ user.publishAttributes(modifiedAttrs);
+ }
+ }
+
+ @Override
+ protected void commit(UserDirectoryWorkingCopy wc) {
+ save();
+ }
+
+ @Override
+ protected void rollback(UserDirectoryWorkingCopy wc) {
+ init();
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.List;
+
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.LdapAttrs;
+import org.osgi.framework.Filter;
+import org.osgi.service.useradmin.User;
+
+public class OsUserDirectory extends AbstractUserDirectory {
+ private final String osUsername = System.getProperty("user.name");
+ private final LdapName osUserDn;
+ private final LdifUser osUser;
+
+ public OsUserDirectory(URI uriArg, Dictionary<String, ?> props) {
+ super(uriArg, props, false);
+ try {
+ osUserDn = new LdapName(LdapAttrs.uid.name() + "=" + osUsername + "," + getUserBase() + "," + getBaseDn());
+ Attributes attributes = new BasicAttributes();
+ attributes.put(LdapAttrs.uid.name(), osUsername);
+ osUser = new LdifUser(this, osUserDn, attributes);
+ } catch (NamingException e) {
+ throw new UserDirectoryException("Cannot create system user", e);
+ }
+ }
+
+ @Override
+ protected List<LdapName> getDirectGroups(LdapName dn) {
+ return new ArrayList<>();
+ }
+
+ @Override
+ protected Boolean daoHasRole(LdapName dn) {
+ return osUserDn.equals(dn);
+ }
+
+ @Override
+ protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException {
+ if (osUserDn.equals(key))
+ return osUser;
+ else
+ throw new NameNotFoundException("Not an OS role");
+ }
+
+ @Override
+ protected List<DirectoryUser> doGetRoles(Filter f) {
+ List<DirectoryUser> res = new ArrayList<>();
+ if (f == null || f.match(osUser.getProperties()))
+ res.add(osUser);
+ return res;
+ }
+
+ @Override
+ protected AbstractUserDirectory scope(User user) {
+ throw new UnsupportedOperationException();
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.NoSuchAlgorithmException;
+import java.security.URIParameter;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+public class OsUserUtils {
+ private static String LOGIN_CONTEXT_USER_NIX = "USER_NIX";
+ private static String LOGIN_CONTEXT_USER_NT = "USER_NT";
+
+ public static String getOsUsername() {
+ return System.getProperty("user.name");
+ }
+
+ public static LoginContext loginAsSystemUser(Subject subject) {
+ try {
+ URL jaasConfigurationUrl = OsUserUtils.class.getClassLoader()
+ .getResource("org/argeo/osgi/useradmin/jaas-os.cfg");
+ URIParameter uriParameter = new URIParameter(jaasConfigurationUrl.toURI());
+ Configuration jaasConfiguration = Configuration.getInstance("JavaLoginConfig", uriParameter);
+ LoginContext lc = new LoginContext(isWindows() ? LOGIN_CONTEXT_USER_NT : LOGIN_CONTEXT_USER_NIX, subject,
+ null, jaasConfiguration);
+ lc.login();
+ return lc;
+ } catch (URISyntaxException | NoSuchAlgorithmException | LoginException e) {
+ throw new RuntimeException("Cannot login as system user", e);
+ }
+ }
+
+ public static void main(String args[]) {
+ Subject subject = new Subject();
+ LoginContext loginContext = loginAsSystemUser(subject);
+ System.out.println(subject);
+ try {
+ loginContext.logout();
+ } catch (LoginException e) {
+ // silent
+ }
+ }
+
+ private static boolean isWindows() {
+ return System.getProperty("os.name").startsWith("Windows");
+ }
+
+ private OsUserUtils() {
+ }
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import static org.argeo.naming.LdapAttrs.description;
+import static org.argeo.naming.LdapAttrs.owner;
+
+import java.security.Principal;
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.security.auth.Subject;
+
+import org.argeo.naming.NamingUtils;
+import org.osgi.service.useradmin.Group;
+
+/**
+ * Canonically implements the Argeo token conventions.
+ */
+public class TokenUtils {
+ public static Set<String> tokensUsed(Subject subject, String tokensBaseDn) {
+ Set<String> res = new HashSet<>();
+ for (Principal principal : subject.getPrincipals()) {
+ String name = principal.getName();
+ if (name.endsWith(tokensBaseDn)) {
+ try {
+ LdapName ldapName = new LdapName(name);
+ String token = ldapName.getRdn(ldapName.size()).getValue().toString();
+ res.add(token);
+ } catch (InvalidNameException e) {
+ throw new UserDirectoryException("Invalid principal " + principal, e);
+ }
+ }
+ }
+ return res;
+ }
+
+ /** The user related to this token group */
+ public static String userDn(Group tokenGroup) {
+ return (String) tokenGroup.getProperties().get(owner.name());
+ }
+
+ public static boolean isExpired(Group tokenGroup) {
+ return isExpired(tokenGroup, Instant.now());
+
+ }
+
+ public static boolean isExpired(Group tokenGroup, Instant instant) {
+ String expiryDateStr = (String) tokenGroup.getProperties().get(description.name());
+ if (expiryDateStr != null) {
+ Instant expiryDate = NamingUtils.ldapDateToInstant(expiryDateStr);
+ if (expiryDate.isBefore(instant)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+// private final String token;
+//
+// public TokenUtils(String token) {
+// this.token = token;
+// }
+//
+// public String getToken() {
+// return token;
+// }
+//
+// @Override
+// public int hashCode() {
+// return token.hashCode();
+// }
+//
+// @Override
+// public boolean equals(Object obj) {
+// if ((obj instanceof TokenUtils) && ((TokenUtils) obj).token.equals(token))
+// return true;
+// return false;
+// }
+//
+// @Override
+// public String toString() {
+// return "Token #" + hashCode();
+// }
+
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+
+import javax.naming.Context;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.NamingUtils;
+
+/** Properties used to configure user admins. */
+public enum UserAdminConf {
+ /** Base DN (cannot be configured externally) */
+ baseDn("dc=example,dc=com"),
+
+ /** URI of the underlying resource (cannot be configured externally) */
+ uri("ldap://localhost:10389"),
+
+ /** User objectClass */
+ userObjectClass("inetOrgPerson"),
+
+ /** Relative base DN for users */
+ userBase("ou=People"),
+
+ /** Groups objectClass */
+ groupObjectClass("groupOfNames"),
+
+ /** Relative base DN for users */
+ groupBase("ou=Groups"),
+
+ /** Read-only source */
+ readOnly(null),
+
+ /** Disabled source */
+ disabled(null),
+
+ /** Authentication realm */
+ realm(null);
+
+ public final static String FACTORY_PID = "org.argeo.osgi.useradmin.config";
+
+ public final static String SCHEME_LDAP = "ldap";
+ public final static String SCHEME_LDAPS = "ldaps";
+ public final static String SCHEME_FILE = "file";
+ public final static String SCHEME_OS = "os";
+ public final static String SCHEME_IPA = "ipa";
+
+ /** The default value. */
+ private Object def;
+
+ UserAdminConf(Object def) {
+ this.def = def;
+ }
+
+ public Object getDefault() {
+ return def;
+ }
+
+ /**
+ * For use as Java property.
+ *
+ * @deprecated use {@link #name()} instead
+ */
+ @Deprecated
+ public String property() {
+ return name();
+ }
+
+ public String getValue(Dictionary<String, ?> properties) {
+ Object res = getRawValue(properties);
+ if (res == null)
+ return null;
+ return res.toString();
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> T getRawValue(Dictionary<String, ?> properties) {
+ Object res = properties.get(name());
+ if (res == null)
+ res = getDefault();
+ return (T) res;
+ }
+
+ /** @deprecated use {@link #valueOf(String)} instead */
+ @Deprecated
+ public static UserAdminConf local(String property) {
+ return UserAdminConf.valueOf(property);
+ }
+
+ /** Hides host and credentials. */
+ public static URI propertiesAsUri(Dictionary<String, ?> properties) {
+ StringBuilder query = new StringBuilder();
+
+ boolean first = true;
+// for (Enumeration<String> keys = properties.keys(); keys.hasMoreElements();) {
+// String key = keys.nextElement();
+// // TODO clarify which keys are relevant (list only the enum?)
+// if (!key.equals("service.factoryPid") && !key.equals("cn") && !key.equals("dn")
+// && !key.equals(Constants.SERVICE_PID) && !key.startsWith("java") && !key.equals(baseDn.name())
+// && !key.equals(uri.name()) && !key.equals(Constants.OBJECTCLASS)
+// && !key.equals(Constants.SERVICE_ID) && !key.equals("bundle.id")) {
+// if (first)
+// first = false;
+// else
+// query.append('&');
+// query.append(valueOf(key).name());
+// query.append('=').append(properties.get(key).toString());
+// }
+// }
+
+ keys: for (UserAdminConf key : UserAdminConf.values()) {
+ if (key.equals(baseDn) || key.equals(uri))
+ continue keys;
+ Object value = properties.get(key.name());
+ if (value == null)
+ continue keys;
+ if (first)
+ first = false;
+ else
+ query.append('&');
+ query.append(key.name());
+ query.append('=').append(value.toString());
+
+ }
+
+ Object bDnObj = properties.get(baseDn.name());
+ String bDn = bDnObj != null ? bDnObj.toString() : null;
+ try {
+ return new URI(null, null, bDn != null ? '/' + bDn : null, query.length() != 0 ? query.toString() : null,
+ null);
+ } catch (URISyntaxException e) {
+ throw new UserDirectoryException("Cannot create URI from properties", e);
+ }
+ }
+
+ public static Dictionary<String, Object> uriAsProperties(String uriStr) {
+ try {
+ Hashtable<String, Object> res = new Hashtable<String, Object>();
+ URI u = new URI(uriStr);
+ String scheme = u.getScheme();
+ if (scheme != null && scheme.equals(SCHEME_IPA)) {
+ return IpaUtils.convertIpaUri(u);
+// scheme = u.getScheme();
+ }
+ String path = u.getPath();
+ // base DN
+ String bDn = path.substring(path.lastIndexOf('/') + 1, path.length());
+ if (bDn.equals("") && SCHEME_OS.equals(scheme)) {
+ bDn = getBaseDnFromHostname();
+ }
+
+ if (bDn.endsWith(".ldif"))
+ bDn = bDn.substring(0, bDn.length() - ".ldif".length());
+
+ // Normalize base DN as LDAP name
+ bDn = new LdapName(bDn).toString();
+
+ String principal = null;
+ String credentials = null;
+ if (scheme != null)
+ if (scheme.equals(SCHEME_LDAP) || scheme.equals(SCHEME_LDAPS)) {
+ // TODO additional checks
+ if (u.getUserInfo() != null) {
+ String[] userInfo = u.getUserInfo().split(":");
+ principal = userInfo.length > 0 ? userInfo[0] : null;
+ credentials = userInfo.length > 1 ? userInfo[1] : null;
+ }
+ } else if (scheme.equals(SCHEME_FILE)) {
+ } else if (scheme.equals(SCHEME_IPA)) {
+ } else if (scheme.equals(SCHEME_OS)) {
+ } else
+ throw new UserDirectoryException("Unsupported scheme " + scheme);
+ Map<String, List<String>> query = NamingUtils.queryToMap(u);
+ for (String key : query.keySet()) {
+ UserAdminConf ldapProp = UserAdminConf.valueOf(key);
+ List<String> values = query.get(key);
+ if (values.size() == 1) {
+ res.put(ldapProp.name(), values.get(0));
+ } else {
+ throw new UserDirectoryException("Only single values are supported");
+ }
+ }
+ res.put(baseDn.name(), bDn);
+ if (SCHEME_OS.equals(scheme))
+ res.put(readOnly.name(), "true");
+ if (principal != null)
+ res.put(Context.SECURITY_PRINCIPAL, principal);
+ if (credentials != null)
+ res.put(Context.SECURITY_CREDENTIALS, credentials);
+ if (scheme != null) {// relative URIs are dealt with externally
+ if (SCHEME_OS.equals(scheme)) {
+ res.put(uri.name(), SCHEME_OS + ":///");
+ } else {
+ URI bareUri = new URI(scheme, null, u.getHost(), u.getPort(),
+ scheme.equals(SCHEME_FILE) ? u.getPath() : null, null, null);
+ res.put(uri.name(), bareUri.toString());
+ }
+ }
+ return res;
+ } catch (Exception e) {
+ throw new UserDirectoryException("Cannot convert " + uri + " to properties", e);
+ }
+ }
+
+ private static String getBaseDnFromHostname() {
+ String hostname;
+ try {
+ hostname = InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ hostname = "localhost.localdomain";
+ }
+ int dotIdx = hostname.indexOf('.');
+ if (dotIdx >= 0) {
+ String domain = hostname.substring(dotIdx + 1, hostname.length());
+ String bDn = ("." + domain).replaceAll("\\.", ",dc=");
+ bDn = bDn.substring(1, bDn.length());
+ return bDn;
+ } else {
+ return "dc=" + hostname;
+ }
+ }
+
+ /**
+ * Hash the base DN in order to have a deterministic string to be used as a cn
+ * for the underlying user directory.
+ */
+ public static String baseDnHash(Dictionary<String, Object> properties) {
+ String bDn = (String) properties.get(baseDn.name());
+ if (bDn == null)
+ throw new UserDirectoryException("No baseDn in " + properties);
+ return DigestUtils.sha1str(bDn);
+ }
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import javax.naming.ldap.LdapName;
+import javax.transaction.xa.XAResource;
+
+/** Information about a user directory. */
+public interface UserDirectory {
+ /** The base DN of all entries in this user directory */
+ LdapName getBaseDn();
+
+ /** The related {@link XAResource} */
+ XAResource getXaResource();
+
+ boolean isReadOnly();
+
+ boolean isDisabled();
+
+ String getUserObjectClass();
+
+ String getUserBase();
+
+ String getGroupObjectClass();
+
+ String getGroupBase();
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import org.osgi.service.useradmin.UserAdmin;
+
+/**
+ * Exceptions related to Argeo's implementation of OSGi {@link UserAdmin}
+ * service.
+ */
+public class UserDirectoryException extends RuntimeException {
+ private static final long serialVersionUID = 1419352360062048603L;
+
+ public UserDirectoryException(String message) {
+ super(message);
+ }
+
+ public UserDirectoryException(String message, Throwable e) {
+ super(message, e);
+ }
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+import javax.transaction.xa.XAResource;
+
+/** {@link XAResource} for a user directory being edited. */
+class UserDirectoryWorkingCopy {
+ // private final static Log log = LogFactory
+ // .getLog(UserDirectoryWorkingCopy.class);
+
+ private Map<LdapName, DirectoryUser> newUsers = new HashMap<LdapName, DirectoryUser>();
+ private Map<LdapName, Attributes> modifiedUsers = new HashMap<LdapName, Attributes>();
+ private Map<LdapName, DirectoryUser> deletedUsers = new HashMap<LdapName, DirectoryUser>();
+
+ void cleanUp() {
+ // clean collections
+ newUsers.clear();
+ newUsers = null;
+ modifiedUsers.clear();
+ modifiedUsers = null;
+ deletedUsers.clear();
+ deletedUsers = null;
+ }
+
+ public boolean noModifications() {
+ return newUsers.size() == 0 && modifiedUsers.size() == 0
+ && deletedUsers.size() == 0;
+ }
+
+ public Attributes getAttributes(LdapName dn) {
+ if (modifiedUsers.containsKey(dn))
+ return modifiedUsers.get(dn);
+ return null;
+ }
+
+ public void startEditing(DirectoryUser user) {
+ LdapName dn = user.getDn();
+ if (modifiedUsers.containsKey(dn))
+ throw new UserDirectoryException("Already editing " + dn);
+ modifiedUsers.put(dn, (Attributes) user.getAttributes().clone());
+ }
+
+ public Map<LdapName, DirectoryUser> getNewUsers() {
+ return newUsers;
+ }
+
+ public Map<LdapName, DirectoryUser> getDeletedUsers() {
+ return deletedUsers;
+ }
+
+ public Map<LdapName, Attributes> getModifiedUsers() {
+ return modifiedUsers;
+ }
+}
--- /dev/null
+package org.argeo.osgi.useradmin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.transaction.xa.XAException;
+import javax.transaction.xa.XAResource;
+import javax.transaction.xa.Xid;
+
+/** {@link XAResource} for a user directory being edited. */
+class WcXaResource implements XAResource {
+ private final AbstractUserDirectory userDirectory;
+
+ private Map<Xid, UserDirectoryWorkingCopy> workingCopies = new HashMap<Xid, UserDirectoryWorkingCopy>();
+ private Xid editingXid = null;
+ private int transactionTimeout = 0;
+
+ public WcXaResource(AbstractUserDirectory userDirectory) {
+ this.userDirectory = userDirectory;
+ }
+
+ @Override
+ public synchronized void start(Xid xid, int flags) throws XAException {
+ if (editingXid != null)
+ throw new UserDirectoryException("Already editing " + editingXid);
+ UserDirectoryWorkingCopy wc = workingCopies.put(xid, new UserDirectoryWorkingCopy());
+ if (wc != null)
+ throw new UserDirectoryException("There is already a working copy for " + xid);
+ this.editingXid = xid;
+ }
+
+ @Override
+ public void end(Xid xid, int flags) throws XAException {
+ checkXid(xid);
+ }
+
+ private UserDirectoryWorkingCopy wc(Xid xid) {
+ return workingCopies.get(xid);
+ }
+
+ synchronized UserDirectoryWorkingCopy wc() {
+ if (editingXid == null)
+ return null;
+ UserDirectoryWorkingCopy wc = workingCopies.get(editingXid);
+ if (wc == null)
+ throw new UserDirectoryException("No working copy found for " + editingXid);
+ return wc;
+ }
+
+ private synchronized void cleanUp(Xid xid) {
+ wc(xid).cleanUp();
+ workingCopies.remove(xid);
+ editingXid = null;
+ }
+
+ @Override
+ public int prepare(Xid xid) throws XAException {
+ checkXid(xid);
+ UserDirectoryWorkingCopy wc = wc(xid);
+ if (wc.noModifications())
+ return XA_RDONLY;
+ try {
+ userDirectory.prepare(wc);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new XAException(XAException.XAER_RMERR);
+ }
+ return XA_OK;
+ }
+
+ @Override
+ public void commit(Xid xid, boolean onePhase) throws XAException {
+ try {
+ checkXid(xid);
+ UserDirectoryWorkingCopy wc = wc(xid);
+ if (wc.noModifications())
+ return;
+ if (onePhase)
+ userDirectory.prepare(wc);
+ userDirectory.commit(wc);
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new XAException(XAException.XAER_RMERR);
+ } finally {
+ cleanUp(xid);
+ }
+ }
+
+ @Override
+ public void rollback(Xid xid) throws XAException {
+ try {
+ checkXid(xid);
+ userDirectory.rollback(wc(xid));
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new XAException(XAException.XAER_RMERR);
+ } finally {
+ cleanUp(xid);
+ }
+ }
+
+ @Override
+ public void forget(Xid xid) throws XAException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isSameRM(XAResource xares) throws XAException {
+ return xares == this;
+ }
+
+ @Override
+ public Xid[] recover(int flag) throws XAException {
+ return new Xid[0];
+ }
+
+ @Override
+ public int getTransactionTimeout() throws XAException {
+ return transactionTimeout;
+ }
+
+ @Override
+ public boolean setTransactionTimeout(int seconds) throws XAException {
+ transactionTimeout = seconds;
+ return true;
+ }
+
+ private void checkXid(Xid xid) throws XAException {
+ if (xid == null)
+ throw new XAException(XAException.XAER_OUTSIDE);
+ if (!xid.equals(xid))
+ throw new XAException(XAException.XAER_NOTA);
+ }
+
+}
--- /dev/null
+USER_NIX {
+ com.sun.security.auth.module.UnixLoginModule requisite;
+};
+
+USER_NT {
+ com.sun.security.auth.module.NTLoginModule requisite;
+};
+
--- /dev/null
+/** LDAP and LDIF based OSGi useradmin implementation. */
+package org.argeo.osgi.useradmin;
\ No newline at end of file
--- /dev/null
+package org.argeo.osgi.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.osgi.resource.Namespace;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Resource;
+
+/** Simplify filtering resources. */
+public class FilterRequirement implements Requirement {
+ private String namespace;
+ private String filter;
+
+ public FilterRequirement(String namespace, String filter) {
+ this.namespace = namespace;
+ this.filter = filter;
+ }
+
+ @Override
+ public Resource getResource() {
+ return null;
+ }
+
+ @Override
+ public String getNamespace() {
+ return namespace;
+ }
+
+ @Override
+ public Map<String, String> getDirectives() {
+ Map<String, String> directives = new HashMap<>();
+ directives.put(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter);
+ return directives;
+ }
+
+ @Override
+ public Map<String, Object> getAttributes() {
+ return new HashMap<>();
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.util;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class OnServiceRegistration<R> implements Future<R> {
+ private BundleContext ownBundleContext = FrameworkUtil.getBundle(OnServiceRegistration.class).getBundleContext();
+
+ private ServiceTracker<?, ?> st;
+
+ private R result;
+ private boolean cancelled = false;
+ private Throwable exception;
+
+ public <T> OnServiceRegistration(Class<T> clss, Function<T, R> function) {
+ this(null, clss, function);
+ }
+
+ public <T> OnServiceRegistration(BundleContext bundleContext, Class<T> clss, Function<T, R> function) {
+ st = new ServiceTracker<T, T>(bundleContext != null ? bundleContext : ownBundleContext, clss, null) {
+
+ @Override
+ public T addingService(ServiceReference<T> reference) {
+ T service = super.addingService(reference);
+ try {
+ if (result != null)// we only want the first one
+ return service;
+ result = function.apply(service);
+ return service;
+ } catch (Exception e) {
+ exception = e;
+ return service;
+ } finally {
+ close();
+ }
+ }
+ };
+ st.open(bundleContext == null);
+ }
+
+ @Override
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ if (result != null || exception != null || cancelled)
+ return false;
+ st.close();
+ cancelled = true;
+ return true;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ @Override
+ public boolean isDone() {
+ return result != null || cancelled;
+ }
+
+ @Override
+ public R get() throws InterruptedException, ExecutionException {
+ st.waitForService(0);
+ return tryGetResult();
+ }
+
+ @Override
+ public R get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+ st.waitForService(TimeUnit.MILLISECONDS.convert(timeout, unit));
+ if (result == null)
+ throw new TimeoutException("No result after " + timeout + " " + unit);
+ return tryGetResult();
+ }
+
+ protected R tryGetResult() throws ExecutionException, CancellationException {
+ if (cancelled)
+ throw new CancellationException();
+ if (exception != null)
+ throw new ExecutionException(exception);
+ if (result == null)// this should not happen
+ try {
+ throw new IllegalStateException("No result available");
+ } catch (Exception e) {
+ exception = e;
+ throw new ExecutionException(e);
+ }
+ return result;
+ }
+
+}
--- /dev/null
+package org.argeo.osgi.util;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ForkJoinPool;
+
+import org.argeo.util.register.Register;
+import org.argeo.util.register.Singleton;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+public class OsgiRegister implements Register {
+ private final BundleContext bundleContext;
+ private Executor executor;
+
+ private CompletableFuture<Void> shutdownStarting = new CompletableFuture<Void>();
+
+ public OsgiRegister(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ // TODO experiment with dedicated executors
+ this.executor = ForkJoinPool.commonPool();
+ }
+
+ @Override
+ public <T> Singleton<T> set(T obj, Class<T> clss, Map<String, Object> attributes, Class<?>... classes) {
+ CompletableFuture<ServiceRegistration<?>> srf = new CompletableFuture<ServiceRegistration<?>>();
+ CompletableFuture<T> postRegistration = CompletableFuture.supplyAsync(() -> {
+ List<String> lst = new ArrayList<>();
+ lst.add(clss.getName());
+ for (Class<?> c : classes) {
+ lst.add(c.getName());
+ }
+ ServiceRegistration<?> sr = bundleContext.registerService(lst.toArray(new String[lst.size()]), obj,
+ new Hashtable<String, Object>(attributes));
+ srf.complete(sr);
+ return obj;
+ }, executor);
+ Singleton<T> singleton = new Singleton<T>(clss, postRegistration);
+
+ shutdownStarting. //
+ thenCompose(singleton::prepareUnregistration). //
+ thenRunAsync(() -> {
+ try {
+ srf.get().unregister();
+ } catch (InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ }
+ }, executor);
+ return singleton;
+ }
+
+ public void shutdown() {
+ shutdownStarting.complete(null);
+ }
+}
--- /dev/null
+package org.argeo.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses a CSV file interpreting the first line as a header. The
+ * {@link #parse(InputStream)} method and the setters are synchronized so that
+ * the object cannot be modified when parsing.
+ */
+public abstract class CsvParser {
+ private char separator = ',';
+ private char quote = '\"';
+
+ private Boolean noHeader = false;
+ private Boolean strictLineAsLongAsHeader = true;
+
+ /**
+ * Actually process a parsed line. If
+ * {@link #setStrictLineAsLongAsHeader(Boolean)} is true (default) the header
+ * and the tokens are guaranteed to have the same size.
+ *
+ * @param lineNumber the current line number, starts at 1 (the header, if header
+ * processing is enabled, the first line otherwise)
+ * @param header the read-only header or null if
+ * {@link #setNoHeader(Boolean)} is true (default is false)
+ * @param tokens the parsed tokens
+ */
+ protected abstract void processLine(Integer lineNumber, List<String> header, List<String> tokens);
+
+ /**
+ * Parses the CSV file (stream is closed at the end)
+ *
+ * @param in the stream to parse
+ *
+ * @deprecated Use {@link #parse(InputStream, Charset)} instead.
+ */
+ @Deprecated
+ public synchronized void parse(InputStream in) {
+ parse(in, (Charset) null);
+ }
+
+ /**
+ * Parses the CSV file (stream is closed at the end)
+ *
+ * @param in the stream to parse
+ * @param encoding the encoding to use.
+ *
+ * @deprecated Use {@link #parse(InputStream, Charset)} instead.
+ */
+ @Deprecated
+ public synchronized void parse(InputStream in, String encoding) {
+ Reader reader;
+ if (encoding == null)
+ reader = new InputStreamReader(in);
+ else
+ try {
+ reader = new InputStreamReader(in, encoding);
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ parse(reader);
+ }
+
+ /**
+ * Parses the CSV file (stream is closed at the end)
+ *
+ * @param in the stream to parse
+ * @param charset the charset to use
+ */
+ public synchronized void parse(InputStream in, Charset charset) {
+ Reader reader;
+ if (charset == null)
+ reader = new InputStreamReader(in);
+ else
+ reader = new InputStreamReader(in, charset);
+ parse(reader);
+ }
+
+ /**
+ * Parses the CSV file (stream is closed at the end)
+ *
+ * @param reader the reader to use (it will be buffered)
+ */
+ public synchronized void parse(Reader reader) {
+ Integer lineCount = 0;
+ try (BufferedReader bufferedReader = new BufferedReader(reader)) {
+ List<String> header = null;
+ if (!noHeader) {
+ String headerStr = bufferedReader.readLine();
+ if (headerStr == null)// empty file
+ return;
+ lineCount++;
+ header = new ArrayList<String>();
+ StringBuffer currStr = new StringBuffer("");
+ Boolean wasInquote = false;
+ while (parseLine(headerStr, header, currStr, wasInquote)) {
+ headerStr = bufferedReader.readLine();
+ if (headerStr == null)
+ break;
+ wasInquote = true;
+ }
+ header = Collections.unmodifiableList(header);
+ }
+
+ String line = null;
+ lines: while ((line = bufferedReader.readLine()) != null) {
+ line = preProcessLine(line);
+ if (line == null) {
+ // skip line
+ continue lines;
+ }
+ lineCount++;
+ List<String> tokens = new ArrayList<String>();
+ StringBuffer currStr = new StringBuffer("");
+ Boolean wasInquote = false;
+ sublines: while (parseLine(line, tokens, currStr, wasInquote)) {
+ line = bufferedReader.readLine();
+ if (line == null)
+ break sublines;
+ wasInquote = true;
+ }
+ if (!noHeader && strictLineAsLongAsHeader) {
+ int headerSize = header.size();
+ int tokenSize = tokens.size();
+ if (tokenSize == 1 && line.trim().equals(""))
+ continue lines;// empty line
+ if (headerSize != tokenSize) {
+ throw new IllegalStateException("Token size " + tokenSize + " is different from header size "
+ + headerSize + " at line " + lineCount + ", line: " + line + ", header: " + header
+ + ", tokens: " + tokens);
+ }
+ }
+ processLine(lineCount, header, tokens);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot parse CSV file (line: " + lineCount + ")", e);
+ }
+ }
+
+ /**
+ * Called before each (logical) line is processed, giving a change to modify it
+ * (typically for cleaning dirty files). To be overridden, return the line
+ * unchanged by default. Skip the line if 'null' is returned.
+ */
+ protected String preProcessLine(String line) {
+ return line;
+ }
+
+ /**
+ * Parses a line character by character for performance purpose
+ *
+ * @return whether to continue parsing this line
+ */
+ protected Boolean parseLine(String str, List<String> tokens, StringBuffer currStr, Boolean wasInquote) {
+ if (wasInquote)
+ currStr.append('\n');
+
+ char[] arr = str.toCharArray();
+ boolean inQuote = wasInquote;
+ for (int i = 0; i < arr.length; i++) {
+ char c = arr[i];
+ if (c == separator) {
+ if (!inQuote) {
+ tokens.add(currStr.toString());
+// currStr.delete(0, currStr.length());
+ currStr.setLength(0);
+ currStr.trimToSize();
+ } else {
+ // we don't remove separator that are in a quoted substring
+ // System.out
+ // .println("IN QUOTE, got a separator: [" + c + "]");
+ currStr.append(c);
+ }
+ } else if (c == quote) {
+ if (inQuote && (i + 1) < arr.length && arr[i + 1] == quote) {
+ // case of double quote
+ currStr.append(quote);
+ i++;
+ } else {// standard
+ inQuote = inQuote ? false : true;
+ }
+ } else {
+ currStr.append(c);
+ }
+ }
+
+ if (!inQuote) {
+ tokens.add(currStr.toString());
+ // System.out.println("# TOKEN: " + currStr);
+ }
+ // if (inQuote)
+ // throw new ArgeoException("Missing quote at the end of the line "
+ // + str + " (parsed: " + tokens + ")");
+ if (inQuote)
+ return true;
+ else
+ return false;
+ // return tokens;
+ }
+
+ public char getSeparator() {
+ return separator;
+ }
+
+ public synchronized void setSeparator(char separator) {
+ this.separator = separator;
+ }
+
+ public char getQuote() {
+ return quote;
+ }
+
+ public synchronized void setQuote(char quote) {
+ this.quote = quote;
+ }
+
+ public Boolean getNoHeader() {
+ return noHeader;
+ }
+
+ public synchronized void setNoHeader(Boolean noHeader) {
+ this.noHeader = noHeader;
+ }
+
+ public Boolean getStrictLineAsLongAsHeader() {
+ return strictLineAsLongAsHeader;
+ }
+
+ public synchronized void setStrictLineAsLongAsHeader(Boolean strictLineAsLongAsHeader) {
+ this.strictLineAsLongAsHeader = strictLineAsLongAsHeader;
+ }
+
+}
--- /dev/null
+package org.argeo.util;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CSV parser allowing to process lines as maps whose keys are the header
+ * fields.
+ */
+public abstract class CsvParserWithLinesAsMap extends CsvParser {
+
+ /**
+ * Actually processes a line.
+ *
+ * @param lineNumber the current line number, starts at 1 (the header, if header
+ * processing is enabled, the first lien otherwise)
+ * @param line the parsed tokens as a map whose keys are the header fields
+ */
+ protected abstract void processLine(Integer lineNumber, Map<String, String> line);
+
+ protected final void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
+ if (header == null)
+ throw new IllegalArgumentException("Only CSV with header is supported");
+ Map<String, String> line = new HashMap<String, String>();
+ for (int i = 0; i < header.size(); i++) {
+ String key = header.get(i);
+ String value = null;
+ if (i < tokens.size())
+ value = tokens.get(i);
+ line.put(key, value);
+ }
+ processLine(lineNumber, line);
+ }
+
+}
--- /dev/null
+package org.argeo.util;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.List;
+
+/** Write in CSV format. */
+public class CsvWriter {
+ private final Writer out;
+
+ private char separator = ',';
+ private char quote = '\"';
+
+ /**
+ * Creates a CSV writer.
+ *
+ * @param out the stream to write to. Caller is responsible for closing it.
+ *
+ * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead.
+ *
+ */
+ @Deprecated
+ public CsvWriter(OutputStream out) {
+ this.out = new OutputStreamWriter(out);
+ }
+
+ /**
+ * Creates a CSV writer.
+ *
+ * @param out the stream to write to. Caller is responsible for closing it.
+ * @param encoding the encoding to use.
+ *
+ * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead.
+ */
+ @Deprecated
+ public CsvWriter(OutputStream out, String encoding) {
+ try {
+ this.out = new OutputStreamWriter(out, encoding);
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Creates a CSV writer.
+ *
+ * @param out the stream to write to. Caller is responsible for closing it.
+ * @param charset the charset to use
+ */
+ public CsvWriter(OutputStream out, Charset charset) {
+ this.out = new OutputStreamWriter(out, charset);
+ }
+
+ /**
+ * Creates a CSV writer.
+ *
+ * @param out the stream to write to. Caller is responsible for closing it.
+ */
+ public CsvWriter(Writer writer) {
+ this.out = writer;
+ }
+
+ /**
+ * Write a CSV line. Also used to write a header if needed (this is transparent
+ * for the CSV writer): simply call it first, before writing the lines.
+ */
+ public void writeLine(List<?> tokens) {
+ try {
+ Iterator<?> it = tokens.iterator();
+ while (it.hasNext()) {
+ Object obj = it.next();
+ writeToken(obj != null ? obj.toString() : null);
+ if (it.hasNext())
+ out.write(separator);
+ }
+ out.write('\n');
+ out.flush();
+ } catch (IOException e) {
+ throw new RuntimeException("Could not write " + tokens, e);
+ }
+ }
+
+ /**
+ * Write a CSV line. Also used to write a header if needed (this is transparent
+ * for the CSV writer): simply call it first, before writing the lines.
+ */
+ public void writeLine(Object[] tokens) {
+ try {
+ for (int i = 0; i < tokens.length; i++) {
+ if (tokens[i] == null) {
+ writeToken(null);
+ } else {
+ writeToken(tokens[i].toString());
+ }
+ if (i != (tokens.length - 1))
+ out.write(separator);
+ }
+ out.write('\n');
+ out.flush();
+ } catch (IOException e) {
+ throw new RuntimeException("Could not write " + tokens, e);
+ }
+ }
+
+ protected void writeToken(String token) throws IOException {
+ if (token == null) {
+ // TODO configure how to deal with null
+ out.write("");
+ return;
+ }
+ // +2 for possible quotes, another +2 assuming there would be an already
+ // quoted string where quotes needs to be duplicated
+ // another +2 for safety
+ // we don't want to increase buffer size while writing
+ StringBuffer buf = new StringBuffer(token.length() + 6);
+ char[] arr = token.toCharArray();
+ boolean shouldQuote = false;
+ for (char c : arr) {
+ if (!shouldQuote) {
+ if (c == separator)
+ shouldQuote = true;
+ if (c == '\n')
+ shouldQuote = true;
+ }
+
+ if (c == quote) {
+ shouldQuote = true;
+ // duplicate quote
+ buf.append(quote);
+ }
+
+ // generic case
+ buf.append(c);
+ }
+
+ if (shouldQuote == true)
+ out.write(quote);
+ out.write(buf.toString());
+ if (shouldQuote == true)
+ out.write(quote);
+ }
+
+ public void setSeparator(char separator) {
+ this.separator = separator;
+ }
+
+ public void setQuote(char quote) {
+ this.quote = quote;
+ }
+
+}
--- /dev/null
+package org.argeo.util;
+
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+/**
+ * Access the keys of a {@link String}-keyed {@link Dictionary} (common throughout
+ * the OSGi APIs) as an {@link Iterable} so that they are easily usable in
+ * for-each loops.
+ */
+class DictionaryKeys implements Iterable<String> {
+ private final Dictionary<String, ?> dictionary;
+
+ public DictionaryKeys(Dictionary<String, ?> dictionary) {
+ this.dictionary = dictionary;
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return new KeyIterator(dictionary.keys());
+ }
+
+ private static class KeyIterator implements Iterator<String> {
+ private final Enumeration<String> keys;
+
+ KeyIterator(Enumeration<String> keys) {
+ this.keys = keys;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return keys.hasMoreElements();
+ }
+
+ @Override
+ public String next() {
+ return keys.nextElement();
+ }
+
+ }
+}
--- /dev/null
+package org.argeo.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/** Utilities around cryptographic digests */
+public class DigestUtils {
+ public final static String MD5 = "MD5";
+ public final static String SHA1 = "SHA1";
+ public final static String SHA256 = "SHA-256";
+ public final static String SHA512 = "SHA-512";
+
+ private static Boolean debug = false;
+ // TODO: make it configurable
+ private final static Integer byteBufferCapacity = 100 * 1024;// 100 KB
+
+ public static byte[] sha1(byte[] bytes) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance(SHA1);
+ digest.update(bytes);
+ byte[] checksum = digest.digest();
+ return checksum;
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static String digest(String algorithm, byte[] bytes) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance(algorithm);
+ digest.update(bytes);
+ byte[] checksum = digest.digest();
+ String res = encodeHexString(checksum);
+ return res;
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
+ }
+ }
+
+ public static String digest(String algorithm, InputStream in) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance(algorithm);
+ // ReadableByteChannel channel = Channels.newChannel(in);
+ // ByteBuffer bb = ByteBuffer.allocateDirect(byteBufferCapacity);
+ // while (channel.read(bb) > 0)
+ // digest.update(bb);
+ byte[] buffer = new byte[byteBufferCapacity];
+ int read = 0;
+ while ((read = in.read(buffer)) > 0) {
+ digest.update(buffer, 0, read);
+ }
+
+ byte[] checksum = digest.digest();
+ String res = encodeHexString(checksum);
+ return res;
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ StreamUtils.closeQuietly(in);
+ }
+ }
+
+ public static String digest(String algorithm, File file) {
+ FileInputStream fis = null;
+ FileChannel fc = null;
+ try {
+ fis = new FileInputStream(file);
+ fc = fis.getChannel();
+
+ // Get the file's size and then map it into memory
+ int sz = (int) fc.size();
+ ByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, sz);
+ return digest(algorithm, bb);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e);
+ } finally {
+ StreamUtils.closeQuietly(fis);
+ if (fc.isOpen())
+ try {
+ fc.close();
+ } catch (IOException e) {
+ // silent
+ }
+ }
+ }
+
+ protected static String digest(String algorithm, ByteBuffer bb) {
+ long begin = System.currentTimeMillis();
+ try {
+ MessageDigest digest = MessageDigest.getInstance(algorithm);
+ digest.update(bb);
+ byte[] checksum = digest.digest();
+ String res = encodeHexString(checksum);
+ long end = System.currentTimeMillis();
+ if (debug)
+ System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s");
+ return res;
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
+ }
+ }
+
+ public static String sha1hex(Path path) {
+ return digest(SHA1, path, byteBufferCapacity);
+ }
+
+ public static String digest(String algorithm, Path path, long bufferSize) {
+ byte[] digest = digestRaw(algorithm, path, bufferSize);
+ return encodeHexString(digest);
+ }
+
+ public static byte[] digestRaw(String algorithm, Path file, long bufferSize) {
+ long begin = System.currentTimeMillis();
+ try {
+ MessageDigest md = MessageDigest.getInstance(algorithm);
+ FileChannel fc = FileChannel.open(file);
+ long fileSize = Files.size(file);
+ if (fileSize <= bufferSize) {
+ ByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, fileSize);
+ md.update(bb);
+ } else {
+ long lastCycle = (fileSize / bufferSize) - 1;
+ long position = 0;
+ for (int i = 0; i <= lastCycle; i++) {
+ ByteBuffer bb;
+ if (i != lastCycle) {
+ bb = fc.map(MapMode.READ_ONLY, position, bufferSize);
+ position = position + bufferSize;
+ } else {
+ bb = fc.map(MapMode.READ_ONLY, position, fileSize - position);
+ position = fileSize;
+ }
+ md.update(bb);
+ }
+ }
+ long end = System.currentTimeMillis();
+ if (debug)
+ System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s");
+ return md.digest();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e);
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot digest " + file + " with algorithm " + algorithm, e);
+ }
+ }
+
+ public static void main(String[] args) {
+ File file;
+ if (args.length > 0)
+ file = new File(args[0]);
+ else {
+ System.err.println("Usage: <file> [<algorithm>]" + " (see http://java.sun.com/j2se/1.5.0/"
+ + "docs/guide/security/CryptoSpec.html#AppA)");
+ return;
+ }
+
+ if (args.length > 1) {
+ String algorithm = args[1];
+ System.out.println(digest(algorithm, file));
+ } else {
+ String algorithm = "MD5";
+ System.out.println(algorithm + ": " + digest(algorithm, file));
+ algorithm = "SHA";
+ System.out.println(algorithm + ": " + digest(algorithm, file));
+ System.out.println(algorithm + ": " + sha1hex(file.toPath()));
+ algorithm = "SHA-256";
+ System.out.println(algorithm + ": " + digest(algorithm, file));
+ algorithm = "SHA-512";
+ System.out.println(algorithm + ": " + digest(algorithm, file));
+ }
+ }
+
+ final private static char[] hexArray = "0123456789abcdef".toCharArray();
+
+ /**
+ * From
+ * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
+ * -a-hex-string-in-java
+ */
+ public static String encodeHexString(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for (int j = 0; j < bytes.length; j++) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+
+}
--- /dev/null
+package org.argeo.util;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Hashes the hashes of the files in a directory. */
+public class DirH {
+
+ private final static Charset charset = Charset.forName("UTF-16");
+ private final static long bufferSize = 200 * 1024 * 1024;
+ private final static String algorithm = "SHA";
+
+ private final static byte EOL = (byte) '\n';
+ private final static byte SPACE = (byte) ' ';
+
+ private final int hashSize;
+
+ private final byte[][] hashes;
+ private final byte[][] fileNames;
+ private final byte[] digest;
+ private final byte[] dirName;
+
+ /**
+ * @param dirName can be null or empty
+ */
+ private DirH(byte[][] hashes, byte[][] fileNames, byte[] dirName) {
+ if (hashes.length != fileNames.length)
+ throw new IllegalArgumentException(hashes.length + " hashes and " + fileNames.length + " file names");
+ this.hashes = hashes;
+ this.fileNames = fileNames;
+ this.dirName = dirName == null ? new byte[0] : dirName;
+ if (hashes.length == 0) {// empty dir
+ hashSize = 20;
+ // FIXME what is the digest of an empty dir?
+ digest = new byte[hashSize];
+ Arrays.fill(digest, SPACE);
+ return;
+ }
+ hashSize = hashes[0].length;
+ for (int i = 0; i < hashes.length; i++) {
+ if (hashes[i].length != hashSize)
+ throw new IllegalArgumentException(
+ "Hash size for " + new String(fileNames[i], charset) + " is " + hashes[i].length);
+ }
+
+ try {
+ MessageDigest md = MessageDigest.getInstance(algorithm);
+ for (int i = 0; i < hashes.length; i++) {
+ md.update(this.hashes[i]);
+ md.update(SPACE);
+ md.update(this.fileNames[i]);
+ md.update(EOL);
+ }
+ digest = md.digest();
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("Cannot digest", e);
+ }
+ }
+
+ public void print(PrintStream out) {
+ out.print(DigestUtils.encodeHexString(digest));
+ if (dirName.length > 0) {
+ out.print(' ');
+ out.print(new String(dirName, charset));
+ }
+ out.print('\n');
+ for (int i = 0; i < hashes.length; i++) {
+ out.print(DigestUtils.encodeHexString(hashes[i]));
+ out.print(' ');
+ out.print(new String(fileNames[i], charset));
+ out.print('\n');
+ }
+ }
+
+ public static DirH digest(Path dir) {
+ try (DirectoryStream<Path> files = Files.newDirectoryStream(dir)) {
+ List<byte[]> hs = new ArrayList<byte[]>();
+ List<String> fNames = new ArrayList<>();
+ for (Path file : files) {
+ if (!Files.isDirectory(file)) {
+ byte[] digest = DigestUtils.digestRaw(algorithm, file, bufferSize);
+ hs.add(digest);
+ fNames.add(file.getFileName().toString());
+ }
+ }
+
+ byte[][] fileNames = new byte[fNames.size()][];
+ for (int i = 0; i < fNames.size(); i++) {
+ fileNames[i] = fNames.get(i).getBytes(charset);
+ }
+ byte[][] hashes = hs.toArray(new byte[hs.size()][]);
+ return new DirH(hashes, fileNames, dir.toString().getBytes(charset));
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot digest " + dir, e);
+ }
+ }
+
+ public static void main(String[] args) {
+ try {
+ DirH dirH = DirH.digest(Paths.get("/home/mbaudier/tmp/"));
+ dirH.print(System.out);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
--- /dev/null
+package org.argeo.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.Temporal;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+/** Utilities around Java basic features. */
+public class LangUtils {
+ /*
+ * NON-API OSGi
+ */
+ /**
+ * Returns an array with the names of the provided classes. Useful when
+ * registering services with multiple interfaces in OSGi.
+ */
+ public static String[] names(Class<?>... clzz) {
+ String[] res = new String[clzz.length];
+ for (int i = 0; i < clzz.length; i++)
+ res[i] = clzz[i].getName();
+ return res;
+ }
+
+ /*
+ * MAP
+ */
+ /**
+ * Creates a new {@link Dictionary} with one key-value pair. Key should not be
+ * null, but if the value is null, it returns an empty {@link Dictionary}.
+ */
+ public static Map<String, Object> map(String key, Object value) {
+ assert key != null;
+ HashMap<String, Object> props = new HashMap<>();
+ if (value != null)
+ props.put(key, value);
+ return props;
+ }
+
+ /*
+ * DICTIONARY
+ */
+
+ /**
+ * Creates a new {@link Dictionary} with one key-value pair. Key should not be
+ * null, but if the value is null, it returns an empty {@link Dictionary}.
+ */
+ public static Dictionary<String, Object> dict(String key, Object value) {
+ assert key != null;
+ Hashtable<String, Object> props = new Hashtable<>();
+ if (value != null)
+ props.put(key, value);
+ return props;
+ }
+
+ /** @deprecated Use {@link #dict(String, Object)} instead. */
+ @Deprecated
+ public static Dictionary<String, Object> dico(String key, Object value) {
+ return dict(key, value);
+ }
+
+ /** Converts a {@link Dictionary} to a {@link Map} of strings. */
+ public static Map<String, String> dictToStringMap(Dictionary<String, ?> properties) {
+ if (properties == null) {
+ return null;
+ }
+ Map<String, String> res = new HashMap<>(properties.size());
+ Enumeration<String> keys = properties.keys();
+ while (keys.hasMoreElements()) {
+ String key = keys.nextElement();
+ res.put(key, properties.get(key).toString());
+ }
+ return res;
+ }
+
+ /**
+ * Get a string property from this map, expecting to find it, or
+ * <code>null</code> if not found.
+ */
+ public static String get(Map<String, ?> map, String key) {
+ Object res = map.get(key);
+ if (res == null)
+ return null;
+ return res.toString();
+ }
+
+ /**
+ * Get a string property from this map, expecting to find it.
+ *
+ * @throws IllegalArgumentException if the key was not found
+ */
+ public static String getNotNull(Map<String, ?> map, String key) {
+ Object res = map.get(key);
+ if (res == null)
+ throw new IllegalArgumentException("Map " + map + " should contain key " + key);
+ return res.toString();
+ }
+
+ /**
+ * Wraps the keys of the provided {@link Dictionary} as an {@link Iterable}.
+ */
+ public static Iterable<String> keys(Dictionary<String, ?> props) {
+ assert props != null;
+ return new DictionaryKeys(props);
+ }
+
+ static String toJson(Dictionary<String, ?> props) {
+ return toJson(props, false);
+ }
+
+ static String toJson(Dictionary<String, ?> props, boolean pretty) {
+ StringBuilder sb = new StringBuilder();
+ sb.append('{');
+ if (pretty)
+ sb.append('\n');
+ Enumeration<String> keys = props.keys();
+ while (keys.hasMoreElements()) {
+ String key = keys.nextElement();
+ if (pretty)
+ sb.append(' ');
+ sb.append('\"').append(key).append('\"');
+ if (pretty)
+ sb.append(" : ");
+ else
+ sb.append(':');
+ sb.append('\"').append(props.get(key)).append('\"');
+ if (keys.hasMoreElements())
+ sb.append(", ");
+ if (pretty)
+ sb.append('\n');
+ }
+ sb.append('}');
+ return sb.toString();
+ }
+
+ static void storeAsProperties(Dictionary<String, Object> props, Path path) throws IOException {
+ if (props == null)
+ throw new IllegalArgumentException("Props cannot be null");
+ Properties toStore = new Properties();
+ for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
+ String key = keys.nextElement();
+ toStore.setProperty(key, props.get(key).toString());
+ }
+ try (OutputStream out = Files.newOutputStream(path)) {
+ toStore.store(out, null);
+ }
+ }
+
+ static void appendAsLdif(String dnBase, String dnKey, Dictionary<String, Object> props, Path path)
+ throws IOException {
+ if (props == null)
+ throw new IllegalArgumentException("Props cannot be null");
+ Object dnValue = props.get(dnKey);
+ String dnStr = dnKey + '=' + dnValue + ',' + dnBase;
+ LdapName dn;
+ try {
+ dn = new LdapName(dnStr);
+ } catch (InvalidNameException e) {
+ throw new IllegalArgumentException("Cannot interpret DN " + dnStr, e);
+ }
+ if (dnValue == null)
+ throw new IllegalArgumentException("DN key " + dnKey + " must have a value");
+ try (Writer writer = Files.newBufferedWriter(path, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) {
+ writer.append("\ndn: ");
+ writer.append(dn.toString());
+ writer.append('\n');
+ for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
+ String key = keys.nextElement();
+ Object value = props.get(key);
+ writer.append(key);
+ writer.append(": ");
+ // FIXME deal with binary and multiple values
+ writer.append(value.toString());
+ writer.append('\n');
+ }
+ }
+ }
+
+ static Dictionary<String, Object> loadFromProperties(Path path) throws IOException {
+ Properties toLoad = new Properties();
+ try (InputStream in = Files.newInputStream(path)) {
+ toLoad.load(in);
+ }
+ Dictionary<String, Object> res = new Hashtable<String, Object>();
+ for (Object key : toLoad.keySet())
+ res.put(key.toString(), toLoad.get(key));
+ return res;
+ }
+
+ /*
+ * COLLECTIONS
+ */
+ /**
+ * Convert a comma-separated separated {@link String} or a {@link String} array
+ * to a {@link List} of {@link String}, trimming them. Useful to quickly
+ * interpret OSGi services properties.
+ *
+ * @return a {@link List} containing the trimmed {@link String}s, or an empty
+ * {@link List} if the argument was <code>null</code>.
+ */
+ public static List<String> toStringList(Object value) {
+ List<String> values = new ArrayList<>();
+ if (value == null)
+ return values;
+ String[] arr;
+ if (value instanceof String) {
+ arr = ((String) value).split(",");
+ } else if (value instanceof String[]) {
+ arr = (String[]) value;
+ } else {
+ throw new IllegalArgumentException("Unsupported value type " + value.getClass());
+ }
+ for (String str : arr) {
+ values.add(str.trim());
+ }
+ return values;
+ }
+
+ /*
+ * EXCEPTIONS
+ */
+ /**
+ * Chain the messages of all causes (one per line, <b>starts with a line
+ * return</b>) without all the stack
+ */
+ public static String chainCausesMessages(Throwable t) {
+ StringBuffer buf = new StringBuffer();
+ chainCauseMessage(buf, t);
+ return buf.toString();
+ }
+
+ /** Recursive chaining of messages */
+ private static void chainCauseMessage(StringBuffer buf, Throwable t) {
+ buf.append('\n').append(' ').append(t.getClass().getCanonicalName()).append(": ").append(t.getMessage());
+ if (t.getCause() != null)
+ chainCauseMessage(buf, t.getCause());
+ }
+
+ /*
+ * TIME
+ */
+ /** Formats time elapsed since start. */
+ public static String since(ZonedDateTime start) {
+ ZonedDateTime now = ZonedDateTime.now();
+ return duration(start, now);
+ }
+
+ /** Formats a duration. */
+ public static String duration(Temporal start, Temporal end) {
+ long count = ChronoUnit.DAYS.between(start, end);
+ if (count != 0)
+ return count > 1 ? count + " days" : count + " day";
+ count = ChronoUnit.HOURS.between(start, end);
+ if (count != 0)
+ return count > 1 ? count + " hours" : count + " hours";
+ count = ChronoUnit.MINUTES.between(start, end);
+ if (count != 0)
+ return count > 1 ? count + " minutes" : count + " minute";
+ count = ChronoUnit.SECONDS.between(start, end);
+ return count > 1 ? count + " seconds" : count + " second";
+ }
+
+ /** Singleton constructor. */
+ private LangUtils() {
+
+ }
+
+}
--- /dev/null
+package org.argeo.util;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+
+/** When OS specific informations are needed. */
+public class OS {
+ public final static OS LOCAL = new OS();
+
+ private final String arch, name, version;
+
+ /** The OS of the running JVM */
+ protected OS() {
+ arch = System.getProperty("os.arch");
+ name = System.getProperty("os.name");
+ version = System.getProperty("os.version");
+ }
+
+ public String getArch() {
+ return arch;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public boolean isMSWindows() {
+ // only MS Windows would use such an horrendous separator...
+ return File.separatorChar == '\\';
+ }
+
+ public String[] getDefaultShellCommand() {
+ if (!isMSWindows())
+ return new String[] { "/bin/sh", "-l", "-i" };
+ else
+ return new String[] { "cmd.exe", "/C" };
+ }
+
+ public static Integer getJvmPid() {
+ /*
+ * This method works on most platforms (including Linux). Although when Java 9
+ * comes along, there is a better way: long pid =
+ * ProcessHandle.current().getPid();
+ *
+ * See:
+ * http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-
+ * process-id
+ */
+ String pidAndHost = ManagementFactory.getRuntimeMXBean().getName();
+ return Integer.parseInt(pidAndHost.substring(0, pidAndHost.indexOf('@')));
+ }
+}
--- /dev/null
+package org.argeo.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class PasswordEncryption {
+ public final static Integer DEFAULT_ITERATION_COUNT = 1024;
+ /** Stronger with 256, but causes problem with Oracle JVM */
+ public final static Integer DEFAULT_SECRETE_KEY_LENGTH = 256;
+ public final static Integer DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED = 128;
+ public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1";
+ public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES";
+ public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding";
+// public final static String DEFAULT_CHARSET = "UTF-8";
+ public final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+
+ private Integer iterationCount = DEFAULT_ITERATION_COUNT;
+ private Integer secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH;
+ private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY;
+ private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION;
+ private String cipherName = DEFAULT_CIPHER_NAME;
+
+ private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
+ (byte) 0x35, (byte) 0xE3, (byte) 0x03 };
+ private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
+ (byte) 0x35, (byte) 0xE3, (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
+ (byte) 0x35, (byte) 0xE3, (byte) 0x03 };
+
+ private Key key;
+ private Cipher ecipher;
+ private Cipher dcipher;
+
+ private String securityProviderName = null;
+
+ /**
+ * This is up to the caller to clear the passed array. Neither copy of nor
+ * reference to the passed array is kept
+ */
+ public PasswordEncryption(char[] password) {
+ this(password, DEFAULT_SALT_8, DEFAULT_IV_16);
+ }
+
+ /**
+ * This is up to the caller to clear the passed array. Neither copies of nor
+ * references to the passed arrays are kept
+ */
+ public PasswordEncryption(char[] password, byte[] passwordSalt, byte[] initializationVector) {
+ try {
+ initKeyAndCiphers(password, passwordSalt, initializationVector);
+ } catch (InvalidKeyException e) {
+ Integer previousSecreteKeyLength = secreteKeyLength;
+ secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED;
+ System.err.println("'" + e.getMessage() + "', will use " + secreteKeyLength
+ + " secrete key length instead of " + previousSecreteKeyLength);
+ try {
+ initKeyAndCiphers(password, passwordSalt, initializationVector);
+ } catch (GeneralSecurityException e1) {
+ throw new IllegalStateException("Cannot get secret key (with restricted length)", e1);
+ }
+ } catch (GeneralSecurityException e) {
+ throw new IllegalStateException("Cannot get secret key", e);
+ }
+ }
+
+ protected void initKeyAndCiphers(char[] password, byte[] passwordSalt, byte[] initializationVector)
+ throws GeneralSecurityException {
+ byte[] salt = new byte[8];
+ System.arraycopy(passwordSalt, 0, salt, 0, salt.length);
+ // for (int i = 0; i < password.length && i < salt.length; i++)
+ // salt[i] = (byte) password[i];
+ byte[] iv = new byte[16];
+ System.arraycopy(initializationVector, 0, iv, 0, iv.length);
+
+ SecretKeyFactory keyFac = SecretKeyFactory.getInstance(getSecretKeyFactoryName());
+ PBEKeySpec keySpec = new PBEKeySpec(password, salt, getIterationCount(), getKeyLength());
+ String secKeyEncryption = getSecretKeyEncryption();
+ if (secKeyEncryption != null) {
+ SecretKey tmp = keyFac.generateSecret(keySpec);
+ key = new SecretKeySpec(tmp.getEncoded(), getSecretKeyEncryption());
+ } else {
+ key = keyFac.generateSecret(keySpec);
+ }
+ if (securityProviderName != null)
+ ecipher = Cipher.getInstance(getCipherName(), securityProviderName);
+ else
+ ecipher = Cipher.getInstance(getCipherName());
+ ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
+ dcipher = Cipher.getInstance(getCipherName());
+ dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
+ }
+
+ public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) throws IOException {
+ try {
+ CipherOutputStream out = new CipherOutputStream(encryptedOut, ecipher);
+ StreamUtils.copy(decryptedIn, out);
+ StreamUtils.closeQuietly(out);
+ } catch (IOException e) {
+ throw e;
+ } finally {
+ StreamUtils.closeQuietly(decryptedIn);
+ }
+ }
+
+ public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) throws IOException {
+ try {
+ CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, dcipher);
+ StreamUtils.copy(decryptedIn, decryptedOut);
+ } catch (IOException e) {
+ throw e;
+ } finally {
+ StreamUtils.closeQuietly(encryptedIn);
+ }
+ }
+
+ public byte[] encryptString(String str) {
+ ByteArrayOutputStream out = null;
+ ByteArrayInputStream in = null;
+ try {
+ out = new ByteArrayOutputStream();
+ in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET));
+ encrypt(in, out);
+ return out.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ StreamUtils.closeQuietly(out);
+ }
+ }
+
+ /** Closes the input stream */
+ public String decryptAsString(InputStream in) {
+ ByteArrayOutputStream out = null;
+ try {
+ out = new ByteArrayOutputStream();
+ decrypt(in, out);
+ return new String(out.toByteArray(), DEFAULT_CHARSET);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ } finally {
+ StreamUtils.closeQuietly(out);
+ }
+ }
+
+ protected Key getKey() {
+ return key;
+ }
+
+ protected Cipher getEcipher() {
+ return ecipher;
+ }
+
+ protected Cipher getDcipher() {
+ return dcipher;
+ }
+
+ protected Integer getIterationCount() {
+ return iterationCount;
+ }
+
+ protected Integer getKeyLength() {
+ return secreteKeyLength;
+ }
+
+ protected String getSecretKeyFactoryName() {
+ return secreteKeyFactoryName;
+ }
+
+ protected String getSecretKeyEncryption() {
+ return secreteKeyEncryption;
+ }
+
+ protected String getCipherName() {
+ return cipherName;
+ }
+
+ public void setIterationCount(Integer iterationCount) {
+ this.iterationCount = iterationCount;
+ }
+
+ public void setSecreteKeyLength(Integer keyLength) {
+ this.secreteKeyLength = keyLength;
+ }
+
+ public void setSecreteKeyFactoryName(String secreteKeyFactoryName) {
+ this.secreteKeyFactoryName = secreteKeyFactoryName;
+ }
+
+ public void setSecreteKeyEncryption(String secreteKeyEncryption) {
+ this.secreteKeyEncryption = secreteKeyEncryption;
+ }
+
+ public void setCipherName(String cipherName) {
+ this.cipherName = cipherName;
+ }
+
+ public void setSecurityProviderName(String securityProviderName) {
+ this.securityProviderName = securityProviderName;
+ }
+}
--- /dev/null
+package org.argeo.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousByteChannel;
+import java.nio.channels.CompletionHandler;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+/** An {@link AsynchronousByteChannel} based on an {@link ExecutorService}. */
+public class ServiceChannel implements AsynchronousByteChannel {
+ private final ReadableByteChannel in;
+ private final WritableByteChannel out;
+
+ private boolean open = true;
+
+ private ExecutorService executor;
+
+ public ServiceChannel(ReadableByteChannel in, WritableByteChannel out, ExecutorService executor) {
+ this.in = in;
+ this.out = out;
+ this.executor = executor;
+ }
+
+ @Override
+ public Future<Integer> read(ByteBuffer dst) {
+ return executor.submit(() -> in.read(dst));
+ }
+
+ @Override
+ public <A> void read(ByteBuffer dst, A attachment, CompletionHandler<Integer, ? super A> handler) {
+ try {
+ Future<Integer> res = read(dst);
+ handler.completed(res.get(), attachment);
+ } catch (Exception e) {
+ handler.failed(e, attachment);
+ }
+ }
+
+ @Override
+ public Future<Integer> write(ByteBuffer src) {
+ return executor.submit(() -> out.write(src));
+ }
+
+ @Override
+ public <A> void write(ByteBuffer src, A attachment, CompletionHandler<Integer, ? super A> handler) {
+ try {
+ Future<Integer> res = write(src);
+ handler.completed(res.get(), attachment);
+ } catch (Exception e) {
+ handler.failed(e, attachment);
+ }
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ try {
+ in.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ try {
+ out.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ open = false;
+ notifyAll();
+ }
+
+ @Override
+ public synchronized boolean isOpen() {
+ return open;
+ }
+
+}
--- /dev/null
+package org.argeo.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+
+/** Utilities to be used when Apache Commons IO is not available. */
+class StreamUtils {
+ private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
+
+ /*
+ * APACHE COMMONS IO (inspired)
+ */
+
+ /** @return the number of bytes */
+ public static Long copy(InputStream in, OutputStream out)
+ throws IOException {
+ Long count = 0l;
+ byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
+ while (true) {
+ int length = in.read(buf);
+ if (length < 0)
+ break;
+ out.write(buf, 0, length);
+ count = count + length;
+ }
+ return count;
+ }
+
+ /** @return the number of chars */
+ public static Long copy(Reader in, Writer out) throws IOException {
+ Long count = 0l;
+ char[] buf = new char[DEFAULT_BUFFER_SIZE];
+ while (true) {
+ int length = in.read(buf);
+ if (length < 0)
+ break;
+ out.write(buf, 0, length);
+ count = count + length;
+ }
+ return count;
+ }
+
+ public static void closeQuietly(InputStream in) {
+ if (in != null)
+ try {
+ in.close();
+ } catch (Exception e) {
+ //
+ }
+ }
+
+ public static void closeQuietly(OutputStream out) {
+ if (out != null)
+ try {
+ out.close();
+ } catch (Exception e) {
+ //
+ }
+ }
+
+ public static void closeQuietly(Reader in) {
+ if (in != null)
+ try {
+ in.close();
+ } catch (Exception e) {
+ //
+ }
+ }
+
+ public static void closeQuietly(Writer out) {
+ if (out != null)
+ try {
+ out.close();
+ } catch (Exception e) {
+ //
+ }
+ }
+}
--- /dev/null
+package org.argeo.util;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/** A generic tester based on Java assertions and functional programming. */
+public class Tester {
+ private Map<String, TesterStatus> results = Collections.synchronizedSortedMap(new TreeMap<>());
+
+ private ClassLoader classLoader;
+
+ /** Use {@link Thread#getContextClassLoader()} by default. */
+ public Tester() {
+ this(Thread.currentThread().getContextClassLoader());
+ }
+
+ public Tester(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ }
+
+ public void execute(String className) {
+ Class<?> clss;
+ try {
+ clss = classLoader.loadClass(className);
+ boolean assertionsEnabled = clss.desiredAssertionStatus();
+ if (!assertionsEnabled)
+ throw new IllegalStateException("Test runner " + getClass().getName()
+ + " requires Java assertions to be enabled. Call the JVM with the -ea argument.");
+ } catch (Exception e1) {
+ throw new IllegalArgumentException("Cannot initalise test for " + className, e1);
+
+ }
+ List<Method> methods = findMethods(clss);
+ if (methods.size() == 0)
+ throw new IllegalArgumentException("No test method found in " + clss);
+ // TODO make order more predictable?
+ for (Method method : methods) {
+ String uid = method.getDeclaringClass().getName() + "#" + method.getName();
+ TesterStatus testStatus = new TesterStatus(uid);
+ Object obj = null;
+ try {
+ beforeTest(uid, method);
+ obj = clss.getDeclaredConstructor().newInstance();
+ method.invoke(obj);
+ testStatus.setPassed();
+ afterTestPassed(uid, method, obj);
+ } catch (Exception e) {
+ testStatus.setFailed(e);
+ afterTestFailed(uid, method, obj, e);
+ } finally {
+ results.put(uid, testStatus);
+ }
+ }
+ }
+
+ protected void beforeTest(String uid, Method method) {
+ // System.out.println(uid + ": STARTING");
+ }
+
+ protected void afterTestPassed(String uid, Method method, Object obj) {
+ System.out.println(uid + ": PASSED");
+ }
+
+ protected void afterTestFailed(String uid, Method method, Object obj, Throwable e) {
+ System.out.println(uid + ": FAILED");
+ e.printStackTrace();
+ }
+
+ protected List<Method> findMethods(Class<?> clss) {
+ List<Method> methods = new ArrayList<Method>();
+// Method call = getMethod(clss, "call");
+// if (call != null)
+// methods.add(call);
+//
+ for (Method method : clss.getMethods()) {
+ if (method.getName().startsWith("test")) {
+ methods.add(method);
+ }
+ }
+ return methods;
+ }
+
+ protected Method getMethod(Class<?> clss, String name, Class<?>... parameterTypes) {
+ try {
+ return clss.getMethod(name, parameterTypes);
+ } catch (NoSuchMethodException e) {
+ return null;
+ } catch (SecurityException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static void main(String[] args) {
+ // deal with arguments
+ String className;
+ if (args.length < 1) {
+ System.err.println(usage());
+ System.exit(1);
+ throw new IllegalArgumentException();
+ } else {
+ className = args[0];
+ }
+
+ Tester test = new Tester();
+ try {
+ test.execute(className);
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+
+ Map<String, TesterStatus> r = test.results;
+ for (String uid : r.keySet()) {
+ TesterStatus testStatus = r.get(uid);
+ System.out.println(testStatus);
+ }
+ }
+
+ public static String usage() {
+ return "java " + Tester.class.getName() + " [test class name]";
+
+ }
+}
--- /dev/null
+package org.argeo.util;
+
+import java.io.Serializable;
+
+/** The status of a test. */
+public class TesterStatus implements Serializable {
+ private static final long serialVersionUID = 6272975746885487000L;
+
+ private Boolean passed = null;
+ private final String uid;
+ private Throwable throwable = null;
+
+ public TesterStatus(String uid) {
+ this.uid = uid;
+ }
+
+ /** For cloning. */
+ public TesterStatus(String uid, Boolean passed, Throwable throwable) {
+ this(uid);
+ this.passed = passed;
+ this.throwable = throwable;
+ }
+
+ public synchronized Boolean isRunning() {
+ return passed == null;
+ }
+
+ public synchronized Boolean isPassed() {
+ assert passed != null;
+ return passed;
+ }
+
+ public synchronized Boolean isFailed() {
+ assert passed != null;
+ return !passed;
+ }
+
+ public synchronized void setPassed() {
+ setStatus(true);
+ }
+
+ public synchronized void setFailed() {
+ setStatus(false);
+ }
+
+ public synchronized void setFailed(Throwable throwable) {
+ setStatus(false);
+ setThrowable(throwable);
+ }
+
+ protected void setStatus(Boolean passed) {
+ if (this.passed != null)
+ throw new IllegalStateException("Passed status of test " + uid + " is already set (to " + passed + ")");
+ this.passed = passed;
+ }
+
+ protected void setThrowable(Throwable throwable) {
+ if (this.throwable != null)
+ throw new IllegalStateException("Throwable of test " + uid + " is already set (to " + passed + ")");
+ this.throwable = throwable;
+ }
+
+ public String getUid() {
+ return uid;
+ }
+
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ // TODO Auto-generated method stub
+ return super.clone();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof TesterStatus) {
+ TesterStatus other = (TesterStatus) o;
+ // we don't check consistency for performance purposes
+ // this equals() is supposed to be used in collections or for transfer
+ return other.uid.equals(uid);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return uid.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return uid + "\t" + (passed ? "passed" : "failed");
+ }
+
+}
--- /dev/null
+package org.argeo.util;
+
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.util.Locale;
+
+/** A throughput, that is, a value per unit of time. */
+public class Throughput {
+ private final static NumberFormat usNumberFormat = NumberFormat.getInstance(Locale.US);
+
+ public enum Unit {
+ s, m, h, d
+ }
+
+ private final Double value;
+ private final Unit unit;
+
+ public Throughput(Double value, Unit unit) {
+ this.value = value;
+ this.unit = unit;
+ }
+
+ public Throughput(Long periodMs, Long count, Unit unit) {
+ if (unit.equals(Unit.s))
+ value = ((double) count * 1000d) / periodMs;
+ else if (unit.equals(Unit.m))
+ value = ((double) count * 60d * 1000d) / periodMs;
+ else if (unit.equals(Unit.h))
+ value = ((double) count * 60d * 60d * 1000d) / periodMs;
+ else if (unit.equals(Unit.d))
+ value = ((double) count * 24d * 60d * 60d * 1000d) / periodMs;
+ else
+ throw new IllegalArgumentException("Unsupported unit " + unit);
+ this.unit = unit;
+ }
+
+ public Throughput(Double value, String unitStr) {
+ this(value, Unit.valueOf(unitStr));
+ }
+
+ public Throughput(String def) {
+ int index = def.indexOf('/');
+ if (def.length() < 3 || index <= 0 || index != def.length() - 2)
+ throw new IllegalArgumentException(
+ def + " no a proper throughput definition" + " (should be <value>/<unit>, e.g. 3.54/s or 1500/h");
+ String valueStr = def.substring(0, index);
+ String unitStr = def.substring(index + 1);
+ try {
+ this.value = usNumberFormat.parse(valueStr).doubleValue();
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Cannot parse " + valueStr + " as a number.", e);
+ }
+ this.unit = Unit.valueOf(unitStr);
+ }
+
+ public Long asMsPeriod() {
+ if (unit.equals(Unit.s))
+ return Math.round(1000d / value);
+ else if (unit.equals(Unit.m))
+ return Math.round((60d * 1000d) / value);
+ else if (unit.equals(Unit.h))
+ return Math.round((60d * 60d * 1000d) / value);
+ else if (unit.equals(Unit.d))
+ return Math.round((24d * 60d * 60d * 1000d) / value);
+ else
+ throw new IllegalArgumentException("Unsupported unit " + unit);
+ }
+
+ @Override
+ public String toString() {
+ return usNumberFormat.format(value) + '/' + unit;
+ }
+
+ public Double getValue() {
+ return value;
+ }
+
+ public Unit getUnit() {
+ return unit;
+ }
+
+}
--- /dev/null
+package org.argeo.util;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.BitSet;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Utilities to simplify and extends usage of {@link UUID}. Only the RFC 4122
+ * variant (also known as Leach–Salz variant) is supported.
+ */
+public class UuidUtils {
+ /** Nil UUID (00000000-0000-0000-0000-000000000000). */
+ public final static UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000");
+ public final static LocalDateTime GREGORIAN_START = LocalDateTime.of(1582, 10, 15, 0, 0, 0);
+
+ private final static long MOST_SIG_VERSION1 = (1l << 12);
+ private final static long LEAST_SIG_RFC4122_VARIANT = (1l << 63);
+
+ private final static SecureRandom RANDOM;
+ private final static AtomicInteger CLOCK_SEQUENCE;
+ private final static byte[] HARDWARE_ADDRESS;
+ /** A start timestamp to which {@link System#nanoTime()}/100 can be added. */
+ private final static long START_TIMESTAMP;
+ static {
+ RANDOM = new SecureRandom();
+ CLOCK_SEQUENCE = new AtomicInteger(RANDOM.nextInt(16384));
+ HARDWARE_ADDRESS = getHardwareAddress();
+
+ long nowVm = System.nanoTime() / 100;
+ Duration duration = Duration.between(GREGORIAN_START, LocalDateTime.now(ZoneOffset.UTC));
+ START_TIMESTAMP = (duration.getSeconds() * 10000000 + duration.getNano() / 100) - nowVm;
+ }
+
+ private static byte[] getHardwareAddress() {
+ InetAddress localHost;
+ try {
+ localHost = InetAddress.getLocalHost();
+ try {
+ NetworkInterface nic = NetworkInterface.getByInetAddress(localHost);
+ return nic.getHardwareAddress();
+ } catch (SocketException e) {
+ return null;
+ }
+ } catch (UnknownHostException e) {
+ return null;
+ }
+
+ }
+
+ public static UUID timeUUIDwithRandomNode() {
+ long timestamp = START_TIMESTAMP + System.nanoTime() / 100;
+ return timeUUID(timestamp, RANDOM);
+ }
+
+ public static UUID timeUUID(long timestamp, Random random) {
+ byte[] node = new byte[6];
+ random.nextBytes(node);
+ node[0] = (byte) (node[0] | 1);
+ long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
+ return timeUUID(timestamp, clockSequence, node);
+ }
+
+ public static UUID timeUUID() {
+ long timestamp = START_TIMESTAMP + System.nanoTime() / 100;
+ return timeUUID(timestamp);
+ }
+
+ public static UUID timeUUID(long timestamp) {
+ if (HARDWARE_ADDRESS == null)
+ return timeUUID(timestamp, RANDOM);
+ long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
+ return timeUUID(timestamp, clockSequence, HARDWARE_ADDRESS);
+ }
+
+ public static UUID timeUUID(long timestamp, NetworkInterface nic) {
+ byte[] node;
+ try {
+ node = nic.getHardwareAddress();
+ } catch (SocketException e) {
+ throw new IllegalStateException("Cannot get hardware address", e);
+ }
+ long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
+ return timeUUID(timestamp, clockSequence, node);
+ }
+
+ public static UUID timeUUID(LocalDateTime time, long clockSequence, byte[] node) {
+ Duration duration = Duration.between(GREGORIAN_START, time);
+ // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000
+ long timestamp = duration.getSeconds() * 10000000 + duration.getNano() / 100;
+ return timeUUID(timestamp, clockSequence, node);
+ }
+
+ public static UUID timeUUID(long timestamp, long clockSequence, byte[] node) {
+ assert node.length >= 6;
+
+ long mostSig = MOST_SIG_VERSION1 // base for version 1 UUID
+ | ((timestamp & 0xFFFFFFFFL) << 32) // time_low
+ | (((timestamp >> 32) & 0xFFFFL) << 16) // time_mid
+ | ((timestamp >> 48) & 0x0FFFL);// time_hi_and_version
+
+ long leastSig = LEAST_SIG_RFC4122_VARIANT // base for Leach–Salz UUID
+ | (((clockSequence & 0x3F00) >> 8) << 56) // clk_seq_hi_res
+ | ((clockSequence & 0xFF) << 48) // clk_seq_low
+ | (node[0] & 0xFFL) //
+ | ((node[1] & 0xFFL) << 8) //
+ | ((node[2] & 0xFFL) << 16) //
+ | ((node[3] & 0xFFL) << 24) //
+ | ((node[4] & 0xFFL) << 32) //
+ | ((node[5] & 0xFFL) << 40); //
+// for (int i = 0; i < 6; i++) {
+// leastSig = leastSig | ((node[i] & 0xFFL) << (8 * i));
+// }
+ UUID uuid = new UUID(mostSig, leastSig);
+
+ // tests
+ assert uuid.node() == BitSet.valueOf(node).toLongArray()[0];
+ assert uuid.timestamp() == timestamp;
+ assert uuid.clockSequence() == clockSequence;
+ assert uuid.version() == 1;
+ assert uuid.variant() == 2;
+ return uuid;
+ }
+
+ @Deprecated
+ public static UUID timeBasedUUID() {
+ return timeBasedUUID(LocalDateTime.now(ZoneOffset.UTC));
+ }
+
+ @Deprecated
+ public static UUID timeBasedRandomUUID() {
+ return timeBasedRandomUUID(LocalDateTime.now(ZoneOffset.UTC), RANDOM);
+ }
+
+ @Deprecated
+ public static UUID timeBasedUUID(LocalDateTime time) {
+ if (HARDWARE_ADDRESS == null)
+ return timeBasedRandomUUID(time, RANDOM);
+ return timeBasedUUID(time, BitSet.valueOf(HARDWARE_ADDRESS));
+ }
+
+ @Deprecated
+ public static UUID timeBasedAddressUUID(LocalDateTime time, NetworkInterface nic) throws SocketException {
+ byte[] nodeBytes = nic.getHardwareAddress();
+ BitSet node = BitSet.valueOf(nodeBytes);
+ return timeBasedUUID(time, node);
+ }
+
+ @Deprecated
+ public static UUID timeBasedRandomUUID(LocalDateTime time, Random random) {
+ byte[] nodeBytes = new byte[6];
+ random.nextBytes(nodeBytes);
+ BitSet node = BitSet.valueOf(nodeBytes);
+ // set random marker
+ node.set(0, true);
+ return timeBasedUUID(time, node);
+ }
+
+ @Deprecated
+ public static UUID timeBasedUUID(LocalDateTime time, BitSet node) {
+ // most significant
+ Duration duration = Duration.between(GREGORIAN_START, time);
+
+ // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000
+ long timeNanos = duration.getSeconds() * 10000000 + duration.getNano() / 100;
+ BitSet timeBits = BitSet.valueOf(new long[] { timeNanos });
+ assert timeBits.length() <= 60;
+
+ int clockSequence;
+ synchronized (CLOCK_SEQUENCE) {
+ clockSequence = CLOCK_SEQUENCE.incrementAndGet();
+ if (clockSequence > 16384)
+ CLOCK_SEQUENCE.set(0);
+ }
+ BitSet clockSequenceBits = BitSet.valueOf(new long[] { clockSequence });
+
+ // Build the UUID, bit by bit
+ // see https://tools.ietf.org/html/rfc4122#section-4.2.2
+ // time
+ BitSet time_low = new BitSet(32);
+ BitSet time_mid = new BitSet(16);
+ BitSet time_hi_and_version = new BitSet(16);
+
+ for (int i = 0; i < 60; i++) {
+ if (i < 32)
+ time_low.set(i, timeBits.get(i));
+ else if (i < 48)
+ time_mid.set(i - 32, timeBits.get(i));
+ else
+ time_hi_and_version.set(i - 48, timeBits.get(i));
+ }
+ // version
+ time_hi_and_version.set(12, true);
+ time_hi_and_version.set(13, false);
+ time_hi_and_version.set(14, false);
+ time_hi_and_version.set(15, false);
+
+ // clock sequence
+ BitSet clk_seq_hi_res = new BitSet(8);
+ BitSet clk_seq_low = new BitSet(8);
+ for (int i = 0; i < 8; i++) {
+ clk_seq_low.set(i, clockSequenceBits.get(i));
+ }
+ for (int i = 8; i < 14; i++) {
+ clk_seq_hi_res.set(i - 8, clockSequenceBits.get(i));
+ }
+ // variant
+ clk_seq_hi_res.set(6, false);
+ clk_seq_hi_res.set(7, true);
+
+// String str = toHexString(time_low.toLongArray()[0]) + "-" + toHexString(time_mid.toLongArray()[0]) + "-"
+// + toHexString(time_hi_and_version.toLongArray()[0]) + "-"
+// + toHexString(clock_seq_hi_and_reserved.toLongArray()[0]) + toHexString(clock_seq_low.toLongArray()[0])
+// + "-" + toHexString(node.toLongArray()[0]);
+// UUID uuid = UUID.fromString(str);
+
+ BitSet uuidBits = new BitSet(128);
+ for (int i = 0; i < 128; i++) {
+ if (i < 48)
+ uuidBits.set(i, node.get(i));
+ else if (i < 56)
+ uuidBits.set(i, clk_seq_low.get(i - 48));
+ else if (i < 64)
+ uuidBits.set(i, clk_seq_hi_res.get(i - 56));
+ else if (i < 80)
+ uuidBits.set(i, time_hi_and_version.get(i - 64));
+ else if (i < 96)
+ uuidBits.set(i, time_mid.get(i - 80));
+ else
+ uuidBits.set(i, time_low.get(i - 96));
+ }
+
+ long[] uuidLongs = uuidBits.toLongArray();
+ assert uuidLongs.length == 2;
+ UUID uuid = new UUID(uuidLongs[1], uuidLongs[0]);
+
+ // tests
+ assert uuid.node() == node.toLongArray()[0];
+ assert uuid.timestamp() == timeNanos;
+ assert uuid.clockSequence() == clockSequence;
+ assert uuid.version() == 1;
+ assert uuid.variant() == 2;
+ return uuid;
+ }
+
+ public static String toBinaryString(UUID uuid, int charsPerSegment, char separator) {
+ String binaryString = toBinaryString(uuid);
+ StringBuilder sb = new StringBuilder(128 + (128 / charsPerSegment));
+ for (int i = 0; i < binaryString.length(); i++) {
+ if (i != 0 && i % charsPerSegment == 0)
+ sb.append(separator);
+ sb.append(binaryString.charAt(i));
+ }
+ return sb.toString();
+ }
+
+ public static String toBinaryString(UUID uuid) {
+ String most = zeroTo64Chars(Long.toBinaryString(uuid.getMostSignificantBits()));
+ String least = zeroTo64Chars(Long.toBinaryString(uuid.getLeastSignificantBits()));
+ String binaryString = most + least;
+ assert binaryString.length() == 128;
+ return binaryString;
+ }
+
+ private static String zeroTo64Chars(String str) {
+ assert str.length() <= 64;
+ if (str.length() < 64) {
+ StringBuilder sb = new StringBuilder(64);
+ for (int i = 0; i < 64 - str.length(); i++)
+ sb.append('0');
+ sb.append(str);
+ return sb.toString();
+ } else
+ return str;
+ }
+
+ public static String compactToStd(String compact) {
+ if (compact.length() != 32)
+ throw new IllegalArgumentException(
+ "Compact UUID '" + compact + "' has length " + compact.length() + " and not 32.");
+ StringBuilder sb = new StringBuilder(36);
+ for (int i = 0; i < 32; i++) {
+ if (i == 8 || i == 12 || i == 16 || i == 20)
+ sb.append('-');
+ sb.append(compact.charAt(i));
+ }
+ String std = sb.toString();
+ assert std.length() == 36;
+ assert UUID.fromString(std).toString().equals(std);
+ return std;
+ }
+
+ public static UUID compactToUuid(String compact) {
+ return UUID.fromString(compactToStd(compact));
+ }
+
+ public static String firstBlock(UUID uuid) {
+ return uuid.toString().substring(0, 8);
+ }
+
+ public static boolean isRandom(UUID uuid) {
+ return uuid.version() == 4;
+ }
+
+ public static boolean isTimeBased(UUID uuid) {
+ return uuid.version() == 1;
+ }
+
+ public static boolean isTimeBasedRandom(UUID uuid) {
+ if (uuid.version() == 1) {
+ BitSet node = BitSet.valueOf(new long[] { uuid.node() });
+ return node.get(0);
+ } else
+ return false;
+ }
+
+ public static boolean isNameBased(UUID uuid) {
+ return uuid.version() == 3 || uuid.version() == 5;
+ }
+
+ /** Singleton. */
+ private UuidUtils() {
+ }
+
+ public final static void main(String[] args) throws Exception {
+ UUID uuid;
+
+// uuid = compactToUuid("996b1f5122de4b2f94e49168d32f22d1");
+// System.out.println(uuid.toString() + ", isRandom=" + isRandom(uuid));
+
+ // warm up before measuring perf
+ for (int i = 0; i < 10; i++) {
+ UUID.randomUUID();
+ timeUUID();
+ timeUUIDwithRandomNode();
+ timeBasedRandomUUID();
+ timeBasedUUID();
+ }
+
+ long begin;
+ long duration;
+
+ begin = System.nanoTime();
+ uuid = UUID.randomUUID();
+ duration = System.nanoTime() - begin;
+ System.out.println(uuid.toString() + " in " + duration + " ns, isRandom=" + isRandom(uuid));
+
+ begin = System.nanoTime();
+ uuid = timeUUID();
+ duration = System.nanoTime() - begin;
+ System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
+
+ begin = System.nanoTime();
+ uuid = timeUUIDwithRandomNode();
+ duration = System.nanoTime() - begin;
+ System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
+
+ begin = System.nanoTime();
+ uuid = timeBasedUUID();
+ duration = System.nanoTime() - begin;
+ System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
+
+ begin = System.nanoTime();
+ uuid = timeBasedRandomUUID();
+ duration = System.nanoTime() - begin;
+ System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
+// System.out.println(toBinaryString(uuid, 8, ' '));
+// System.out.println(toBinaryString(uuid, 16, '\n'));
+ }
+}
--- /dev/null
+/** Generic Java utilities. */
+package org.argeo.util;
\ No newline at end of file
--- /dev/null
+package org.argeo.util.register;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+/**
+ * A wrapper for an object, whose dependencies and life cycle can be managed.
+ */
+public class Component<I> {
+
+ private final I instance;
+
+ private final Runnable init;
+ private final Runnable close;
+
+ private final Map<Class<? super I>, PublishedType<? super I>> types;
+ private final Set<Dependency<?>> dependencies;
+
+ private CompletableFuture<Void> activationStarted = null;
+ private CompletableFuture<Void> activated = null;
+
+ private CompletableFuture<Void> deactivationStarted = null;
+ private CompletableFuture<Void> deactivated = null;
+
+ private Set<Dependency<?>> dependants = new HashSet<>();
+
+ Component(Consumer<Component<?>> register, I instance, Runnable init, Runnable close,
+ Set<Dependency<?>> dependencies, Set<Class<? super I>> classes) {
+ assert instance != null;
+ assert init != null;
+ assert close != null;
+ assert dependencies != null;
+ assert classes != null;
+
+ this.instance = instance;
+ this.init = init;
+ this.close = close;
+
+ // types
+ Map<Class<? super I>, PublishedType<? super I>> types = new HashMap<>(classes.size());
+ for (Class<? super I> clss : classes) {
+// if (!clss.isAssignableFrom(instance.getClass()))
+// throw new IllegalArgumentException(
+// "Type " + clss.getName() + " is not compatible with " + instance.getClass().getName());
+ types.put(clss, new PublishedType<>(this, clss));
+ }
+ this.types = Collections.unmodifiableMap(types);
+
+ // dependencies
+ this.dependencies = Collections.unmodifiableSet(new HashSet<>(dependencies));
+ for (Dependency<?> dependency : this.dependencies) {
+ dependency.setDependantComponent(this);
+ }
+
+ // deactivated by default
+ deactivated = CompletableFuture.completedFuture(null);
+ deactivationStarted = CompletableFuture.completedFuture(null);
+
+ // TODO check whether context is active, so that we start right away
+ prepareNextActivation();
+
+ register.accept(this);
+ }
+
+ private void prepareNextActivation() {
+ activationStarted = new CompletableFuture<Void>();
+ activated = activationStarted //
+ .thenComposeAsync(this::dependenciesActivated) //
+ .thenRun(this.init) //
+ .thenRun(() -> prepareNextDeactivation());
+ }
+
+ private void prepareNextDeactivation() {
+ deactivationStarted = new CompletableFuture<Void>();
+ deactivated = deactivationStarted //
+ .thenComposeAsync(this::dependantsDeactivated) //
+ .thenRun(this.close) //
+ .thenRun(() -> prepareNextActivation());
+ }
+
+ public CompletableFuture<Void> getActivated() {
+ return activated;
+ }
+
+ public CompletableFuture<Void> getDeactivated() {
+ return deactivated;
+ }
+
+ void startActivating() {
+ if (activated.isDone() || activationStarted.isDone())
+ return;
+ activationStarted.complete(null);
+ }
+
+ void startDeactivating() {
+ if (deactivated.isDone() || deactivationStarted.isDone())
+ return;
+ deactivationStarted.complete(null);
+ }
+
+ CompletableFuture<Void> dependenciesActivated(Void v) {
+ Set<CompletableFuture<?>> constraints = new HashSet<>(this.dependencies.size());
+ for (Dependency<?> dependency : this.dependencies) {
+ CompletableFuture<Void> dependencyActivated = dependency.publisherActivated() //
+ .thenCompose(dependency::set);
+ constraints.add(dependencyActivated);
+ }
+ return CompletableFuture.allOf(constraints.toArray(new CompletableFuture[constraints.size()]));
+ }
+
+ CompletableFuture<Void> dependantsDeactivated(Void v) {
+ Set<CompletableFuture<?>> constraints = new HashSet<>(this.dependants.size());
+ for (Dependency<?> dependant : this.dependants) {
+ CompletableFuture<Void> dependantDeactivated = dependant.dependantDeactivated() //
+ .thenCompose(dependant::unset);
+ constraints.add(dependantDeactivated);
+ }
+ CompletableFuture<Void> dependantsDeactivated = CompletableFuture
+ .allOf(constraints.toArray(new CompletableFuture[constraints.size()]));
+ return dependantsDeactivated;
+
+ }
+
+ void addDependant(Dependency<?> dependant) {
+ dependants.add(dependant);
+ }
+
+ I getInstance() {
+ return instance;
+ }
+
+ @SuppressWarnings("unchecked")
+ <T> PublishedType<T> getType(Class<T> clss) {
+ if (!types.containsKey(clss))
+ throw new IllegalArgumentException(clss.getName() + " is not a type published by this component");
+ return (PublishedType<T>) types.get(clss);
+ }
+
+ <T> boolean isPublishedType(Class<T> clss) {
+ return types.containsKey(clss);
+ }
+
+ public static class PublishedType<T> {
+ private Component<? extends T> component;
+ private Class<T> clss;
+
+// private CompletableFuture<Component<? extends T>> publisherAvailable;
+ private CompletableFuture<T> value;
+
+ public PublishedType(Component<? extends T> component, Class<T> clss) {
+ this.clss = clss;
+ this.component = component;
+ value = CompletableFuture.completedFuture((T) component.instance);
+// value = publisherAvailable.thenApply((c) -> c.getInstance());
+ }
+
+ Component<?> getPublisher() {
+ return component;
+ }
+
+// CompletableFuture<Component<? extends T>> publisherAvailable() {
+// return publisherAvailable;
+// }
+
+ Class<T> getType() {
+ return clss;
+ }
+ }
+
+ public static class Builder<I> {
+ private final I instance;
+
+ private Runnable init;
+ private Runnable close;
+
+ private Set<Dependency<?>> dependencies = new HashSet<>();
+ private Set<Class<? super I>> types = new HashSet<>();
+
+ public Builder(I instance) {
+ this.instance = instance;
+ }
+
+ public Component<I> build(Consumer<Component<?>> register) {
+ // default values
+ if (types.isEmpty()) {
+ types.add(getInstanceClass());
+ }
+
+ if (init == null)
+ init = () -> {
+ };
+ if (close == null)
+ close = () -> {
+ };
+
+ // instantiation
+ Component<I> component = new Component<I>(register, instance, init, close, dependencies, types);
+ for (Dependency<?> dependency : dependencies) {
+ dependency.type.getPublisher().addDependant(dependency);
+ }
+ return component;
+ }
+
+ public Builder<I> addType(Class<? super I> clss) {
+ types.add(clss);
+ return this;
+ }
+
+ public Builder<I> addInit(Runnable init) {
+ if (this.init != null)
+ throw new IllegalArgumentException("init method is already set");
+ this.init = init;
+ return this;
+ }
+
+ public Builder<I> addClose(Runnable close) {
+ if (this.close != null)
+ throw new IllegalArgumentException("close method is already set");
+ this.close = close;
+ return this;
+ }
+
+ public <D> Builder<I> addDependency(PublishedType<D> type, Consumer<D> set, Consumer<D> unset) {
+ dependencies.add(new Dependency<D>(type, set, unset));
+ return this;
+ }
+
+ public I get() {
+ return instance;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Class<I> getInstanceClass() {
+ return (Class<I>) instance.getClass();
+ }
+
+ }
+
+ static class Dependency<D> {
+ private PublishedType<D> type;
+ private Consumer<D> set;
+ private Consumer<D> unset;
+
+ // live
+ Component<?> dependantComponent;
+ CompletableFuture<Void> setStage;
+ CompletableFuture<Void> unsetStage;
+
+ public Dependency(PublishedType<D> types, Consumer<D> set, Consumer<D> unset) {
+ super();
+ this.type = types;
+ this.set = set;
+ this.unset = unset != null ? unset : (v) -> set.accept(null);
+ }
+
+ // live
+ void setDependantComponent(Component<?> component) {
+ this.dependantComponent = component;
+ }
+
+ CompletableFuture<Void> publisherActivated() {
+ return type.getPublisher().activated.copy();
+ }
+
+ CompletableFuture<Void> dependantDeactivated() {
+ return dependantComponent.deactivated.copy();
+ }
+
+ CompletableFuture<Void> set(Void v) {
+ return type.value.thenAccept(set);
+ }
+
+ CompletableFuture<Void> unset(Void v) {
+ return type.value.thenAccept(unset);
+ }
+
+ }
+}
--- /dev/null
+package org.argeo.util.register;
+
+import java.util.Map;
+
+/** A dynamic register of objects. */
+public interface Register {
+ <T> Singleton<T> set(T obj, Class<T> clss, Map<String, Object> attributes, Class<?>... classes);
+
+ void shutdown();
+}
--- /dev/null
+package org.argeo.util.register;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+
+public class Singleton<T> {
+ private final Class<T> clss;
+ private final CompletableFuture<T> registrationStage;
+ private final List<Consumer<T>> unregistrationHooks = new ArrayList<>();
+
+ public Singleton(Class<T> clss, CompletableFuture<T> registrationStage) {
+ this.clss = clss;
+ this.registrationStage = registrationStage;
+ }
+
+ CompletionStage<T> getRegistrationStage() {
+ return registrationStage.minimalCompletionStage();
+ }
+
+ public void addUnregistrationHook(Consumer<T> todo) {
+ unregistrationHooks.add(todo);
+ }
+
+ public Future<T> getValue() {
+ return registrationStage.copy();
+ }
+
+ public CompletableFuture<Void> prepareUnregistration(Void v) {
+ List<CompletableFuture<Void>> lst = new ArrayList<>();
+ for (Consumer<T> hook : unregistrationHooks) {
+ lst.add(registrationStage.thenAcceptAsync(hook));
+ }
+ CompletableFuture<Void> prepareUnregistrationStage = CompletableFuture
+ .allOf(lst.toArray(new CompletableFuture<?>[lst.size()]));
+ return prepareUnregistrationStage;
+ }
+}
--- /dev/null
+package org.argeo.util.register;
+
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+/** A minimal component register. */
+public class StaticRegister {
+ private final static AtomicBoolean started = new AtomicBoolean(false);
+ private final static IdentityHashMap<Object, Component<?>> components = new IdentityHashMap<>();
+
+ public static Consumer<Component<?>> asConsumer() {
+ return (c) -> registerComponent(c);
+ }
+
+// public static BiFunction<Class<?>, Predicate<Map<String, Object>>, Component<?>> asProvider() {
+//
+// }
+
+ static synchronized <T> Component<? extends T> find(Class<T> clss, Predicate<Map<String, Object>> filter) {
+ Set<Component<? extends T>> result = new HashSet<>();
+ instances: for (Object instance : components.keySet()) {
+ if (!clss.isAssignableFrom(instance.getClass()))
+ continue instances;
+ Component<? extends T> component = (Component<? extends T>) components.get(instance);
+
+ // TODO filter
+ if (component.isPublishedType(clss))
+ result.add(component);
+ }
+ if (result.isEmpty())
+ return null;
+ return result.iterator().next();
+
+ }
+
+ static synchronized void registerComponent(Component<?> component) {
+ if (started.get()) // TODO make it really dynamic
+ throw new IllegalStateException("Already activated");
+ if (components.containsKey(component.getInstance()))
+ throw new IllegalArgumentException("Already registered as component");
+ components.put(component.getInstance(), component);
+ }
+
+ static synchronized Component<?> get(Object instance) {
+ if (!components.containsKey(instance))
+ throw new IllegalArgumentException("Not registered as component");
+ return components.get(instance);
+ }
+
+ synchronized static void activate() {
+ if (started.get())
+ throw new IllegalStateException("Already activated");
+ Set<CompletableFuture<?>> constraints = new HashSet<>();
+ for (Component<?> component : components.values()) {
+ component.startActivating();
+ constraints.add(component.getActivated());
+ }
+
+ // wait
+ try {
+ CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(true))
+ .get();
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ synchronized static void deactivate() {
+ if (!started.get())
+ throw new IllegalStateException("Not activated");
+ Set<CompletableFuture<?>> constraints = new HashSet<>();
+ for (Component<?> component : components.values()) {
+ component.startDeactivating();
+ constraints.add(component.getDeactivated());
+ }
+
+ // wait
+ try {
+ CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(false))
+ .get();
+ } catch (ExecutionException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ synchronized static void clear() {
+ components.clear();
+ }
+
+}
<modules>
<!-- Base -->
<module>org.argeo.init</module>
- <module>org.argeo.enterprise</module>
+ <module>org.argeo.util</module>
<!-- Eclipse -->
<!-- CMS -->
<module>org.argeo.api</module>
<module>rap</module>
<!-- Distribution -->
<module>dep</module>
- <module>demo</module>
<module>dist</module>
<module>sdk</module>
</modules>