Massive package refactoring
authorMathieu <mbaudier@argeo.org>
Sat, 5 Nov 2022 06:52:27 +0000 (07:52 +0100)
committerMathieu <mbaudier@argeo.org>
Sat, 5 Nov 2022 06:52:27 +0000 (07:52 +0100)
285 files changed:
Makefile
org.argeo.api.acr/src/org/argeo/api/acr/ArgeoNamespace.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java
org.argeo.api.acr/src/org/argeo/api/acr/CrName.java
org.argeo.api.acr/src/org/argeo/api/acr/RuntimeNamespaceContext.java
org.argeo.api.acr/src/org/argeo/api/acr/ldap/Distinguished.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapAttrs.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapObjs.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/ldap/NamingUtils.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/ldap/NodeOID.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/ldap/SpecifiedName.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/tabular/ArrayTabularRow.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularColumn.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularContent.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRow.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRowIterator.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularWriter.java [new file with mode: 0644]
org.argeo.api.acr/src/org/argeo/api/acr/tabular/package-info.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/directory/Directory.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/directory/DirectoryDigestUtils.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/directory/HierarchyUnit.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/keyring/CryptoKeyring.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/keyring/Keyring.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/keyring/PBEKeySpecCallback.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/keyring/package-info.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/AbstractWorkingCopy.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/JtaStatusAdapter.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleRollbackException.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransaction.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransactionManager.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/TransactionStatusAdapter.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/UuidXid.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkContext.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkControl.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkTransaction.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopy.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyProcessor.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyXaResource.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/XAResourceProvider.java [new file with mode: 0644]
org.argeo.api.cms/src/org/argeo/api/cms/transaction/package-info.java [new file with mode: 0644]
org.argeo.api.register/.classpath [new file with mode: 0644]
org.argeo.api.register/.project [new file with mode: 0644]
org.argeo.api.register/META-INF/.gitignore [new file with mode: 0644]
org.argeo.api.register/bnd.bnd [new file with mode: 0644]
org.argeo.api.register/build.properties [new file with mode: 0644]
org.argeo.api.register/src/org/argeo/api/register/Component.java [new file with mode: 0644]
org.argeo.api.register/src/org/argeo/api/register/ComponentRegister.java [new file with mode: 0644]
org.argeo.api.register/src/org/argeo/api/register/RankingKey.java [new file with mode: 0644]
org.argeo.api.register/src/org/argeo/api/register/SimpleRegister.java [new file with mode: 0644]
org.argeo.cms.ee/bnd.bnd
org.argeo.cms.ee/src/org/argeo/cms/integration/CmsExceptionsChain.java
org.argeo.cms.ee/src/org/argeo/cms/integration/CmsTokenServlet.java
org.argeo.cms.ee/src/org/argeo/cms/servlet/CmsServletContext.java
org.argeo.cms.ee/src/org/argeo/cms/servlet/internal/PkgServlet.java
org.argeo.cms.ee/src/org/argeo/cms/websocket/server/TestEndpoint.java
org.argeo.cms.lib.jetty/src/org/argeo/cms/jetty/JettyHttpServer.java
org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/BasicSshServer.java
org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/SshSync.java
org.argeo.cms.ux/src/org/argeo/cms/ux/CmsUxUtils.java
org.argeo.cms/OSGI-INF/cmsUserAdmin.xml
org.argeo.cms/OSGI-INF/cmsUserManager.xml
org.argeo.cms/OSGI-INF/transactionManager.xml
org.argeo.cms/src/org/argeo/cms/AbstractKeyring.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsUserManager.java
org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java
org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java
org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java
org.argeo.cms/src/org/argeo/cms/acr/CmsContentTypes.java
org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java
org.argeo.cms/src/org/argeo/cms/acr/SingleUserContentRepository.java
org.argeo.cms/src/org/argeo/cms/acr/dav/DavContent.java
org.argeo.cms/src/org/argeo/cms/acr/dav/DavContentProvider.java
org.argeo.cms/src/org/argeo/cms/acr/directory/AbstractDirectoryContent.java
org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java
org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java
org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java
org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java
org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java
org.argeo.cms/src/org/argeo/cms/acr/fs/FsContentProvider.java
org.argeo.cms/src/org/argeo/cms/acr/xml/ElementIterator.java
org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java
org.argeo.cms/src/org/argeo/cms/auth/CmsRole.java
org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java
org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java
org.argeo.cms/src/org/argeo/cms/auth/RemoteAuthUtils.java
org.argeo.cms/src/org/argeo/cms/auth/RemoteSessionLoginModule.java
org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java
org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java
org.argeo.cms/src/org/argeo/cms/auth/UserAdminUtils.java
org.argeo.cms/src/org/argeo/cms/client/CmsClient.java
org.argeo.cms/src/org/argeo/cms/dav/DavClient.java
org.argeo.cms/src/org/argeo/cms/dav/DavDepth.java
org.argeo.cms/src/org/argeo/cms/dav/DavHttpHandler.java
org.argeo.cms/src/org/argeo/cms/dav/DavResponse.java
org.argeo.cms/src/org/argeo/cms/dav/MultiStatusReader.java
org.argeo.cms/src/org/argeo/cms/dav/MultiStatusWriter.java
org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectory.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectoryDao.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/AttributesDictionary.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/AuthPassword.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/DefaultLdapEntry.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/IpaUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapConnection.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDao.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDirectoryDao.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntry.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntryWorkingCopy.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapHierarchyUnit.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapNameUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifDao.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifParser.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifWriter.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/directory/ldap/SharedSecret.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dns/DnsBrowser.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/dns/SrvRecord.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/file/ChecksumFactory.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/http/HttpMethod.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/http/HttpServerUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/http/HttpStatus.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java
org.argeo.cms/src/org/argeo/cms/internal/auth/ImpliedByPrincipal.java
org.argeo.cms/src/org/argeo/cms/internal/osgi/CmsOsgiLogger.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsAcrHttpHandler.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java
org.argeo.cms/src/org/argeo/cms/osgi/BundleCmsTheme.java
org.argeo.cms/src/org/argeo/cms/osgi/FilterRequirement.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingAuthorization.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingUserAdmin.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AuthenticatingUser.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryGroup.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUser.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUserAdmin.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifAuthorization.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifGroup.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifUser.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserDirectory.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/TokenUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/UserDirectory.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/jaas-os.cfg [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/osgi/useradmin/package-info.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/runtime/DirectoryConf.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java
org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java [deleted file]
org.argeo.cms/src/org/argeo/cms/security/ChecksumFactory.java [deleted file]
org.argeo.cms/src/org/argeo/cms/security/CryptoKeyring.java [deleted file]
org.argeo.cms/src/org/argeo/cms/security/Keyring.java [deleted file]
org.argeo.cms/src/org/argeo/cms/security/PBEKeySpecCallback.java [deleted file]
org.argeo.cms/src/org/argeo/cms/security/package-info.java [deleted file]
org.argeo.cms/src/org/argeo/cms/tabular/ArrayTabularRow.java [deleted file]
org.argeo.cms/src/org/argeo/cms/tabular/TabularColumn.java [deleted file]
org.argeo.cms/src/org/argeo/cms/tabular/TabularContent.java [deleted file]
org.argeo.cms/src/org/argeo/cms/tabular/TabularRow.java [deleted file]
org.argeo.cms/src/org/argeo/cms/tabular/TabularRowIterator.java [deleted file]
org.argeo.cms/src/org/argeo/cms/tabular/TabularWriter.java [deleted file]
org.argeo.cms/src/org/argeo/cms/tabular/package-info.java [deleted file]
org.argeo.cms/src/org/argeo/cms/util/CompositeString.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/CsvParser.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/CsvParserWithLinesAsMap.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/CsvWriter.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/CurrentSubject.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/DictionaryKeys.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/DigestUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/DirH.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/ExceptionsChain.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/FsUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/LangUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/OS.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/PasswordEncryption.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/ServiceChannel.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/Tester.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/TesterStatus.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/Throughput.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/util/package-info.java [new file with mode: 0644]
org.argeo.util/.classpath [deleted file]
org.argeo.util/.project [deleted file]
org.argeo.util/META-INF/.gitignore [deleted file]
org.argeo.util/bnd.bnd [deleted file]
org.argeo.util/build.properties [deleted file]
org.argeo.util/src/org/argeo/osgi/internal/EnterpriseActivator.java [deleted file]
org.argeo.util/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java [deleted file]
org.argeo.util/src/org/argeo/osgi/provisioning/package-info.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/LdifAuthorization.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/OsUserUtils.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/TokenUtils.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/jaas-os.cfg [deleted file]
org.argeo.util/src/org/argeo/osgi/useradmin/package-info.java [deleted file]
org.argeo.util/src/org/argeo/osgi/util/FilterRequirement.java [deleted file]
org.argeo.util/src/org/argeo/osgi/util/OnServiceRegistration.java [deleted file]
org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java [deleted file]
org.argeo.util/src/org/argeo/util/CompositeString.java [deleted file]
org.argeo.util/src/org/argeo/util/CsvParser.java [deleted file]
org.argeo.util/src/org/argeo/util/CsvParserWithLinesAsMap.java [deleted file]
org.argeo.util/src/org/argeo/util/CsvWriter.java [deleted file]
org.argeo.util/src/org/argeo/util/CurrentSubject.java [deleted file]
org.argeo.util/src/org/argeo/util/DictionaryKeys.java [deleted file]
org.argeo.util/src/org/argeo/util/DigestUtils.java [deleted file]
org.argeo.util/src/org/argeo/util/DirH.java [deleted file]
org.argeo.util/src/org/argeo/util/ExceptionsChain.java [deleted file]
org.argeo.util/src/org/argeo/util/FsUtils.java [deleted file]
org.argeo.util/src/org/argeo/util/LangUtils.java [deleted file]
org.argeo.util/src/org/argeo/util/OS.java [deleted file]
org.argeo.util/src/org/argeo/util/PasswordEncryption.java [deleted file]
org.argeo.util/src/org/argeo/util/ServiceChannel.java [deleted file]
org.argeo.util/src/org/argeo/util/StreamUtils.java [deleted file]
org.argeo.util/src/org/argeo/util/Tester.java [deleted file]
org.argeo.util/src/org/argeo/util/TesterStatus.java [deleted file]
org.argeo.util/src/org/argeo/util/Throughput.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/Directory.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/DirectoryConf.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/DirectoryDigestUtils.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectoryDao.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/AttributesDictionary.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/AuthPassword.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/IpaUtils.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapConnection.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntryWorkingCopy.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/LdapNameUtils.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/LdifParser.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/LdifWriter.java [deleted file]
org.argeo.util/src/org/argeo/util/directory/ldap/SharedSecret.java [deleted file]
org.argeo.util/src/org/argeo/util/http/HttpHeader.java [deleted file]
org.argeo.util/src/org/argeo/util/http/HttpMethod.java [deleted file]
org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java [deleted file]
org.argeo.util/src/org/argeo/util/http/HttpStatus.java [deleted file]
org.argeo.util/src/org/argeo/util/internal/DisplayQName.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/Distinguished.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/LdapAttrs.csv [deleted file]
org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/LdapObjs.csv [deleted file]
org.argeo.util/src/org/argeo/util/naming/LdapObjs.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/NamingUtils.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/NodeOID.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/SpecifiedName.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/dns/SrvRecord.java [deleted file]
org.argeo.util/src/org/argeo/util/naming/package-info.java [deleted file]
org.argeo.util/src/org/argeo/util/package-info.java [deleted file]
org.argeo.util/src/org/argeo/util/register/Component.java [deleted file]
org.argeo.util/src/org/argeo/util/register/ComponentRegister.java [deleted file]
org.argeo.util/src/org/argeo/util/register/RankingKey.java [deleted file]
org.argeo.util/src/org/argeo/util/register/SimpleRegister.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/AbstractWorkingCopy.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/JtaStatusAdapter.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/SimpleRollbackException.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/SimpleTransaction.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/SimpleTransactionManager.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/TransactionStatusAdapter.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/UuidXid.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/WorkContext.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/WorkControl.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/WorkTransaction.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/WorkingCopy.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/WorkingCopyProcessor.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/WorkingCopyXaResource.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/XAResourceProvider.java [deleted file]
org.argeo.util/src/org/argeo/util/transaction/package-info.java [deleted file]
osgi/equinox/org.argeo.cms.lib.equinox/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java
swt/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangePassword.java
swt/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseWorkbench.java
swt/org.argeo.cms.swt/src/org/argeo/cms/swt/AbstractSwtCmsView.java
swt/rap/org.argeo.cms.swt.rap/src/org/argeo/cms/web/CmsWebApp.java
swt/rcp/org.argeo.cms.swt.rcp/src/org/argeo/cms/ui/rcp/CmsRcpDisplayFactory.java
swt/rcp/org.argeo.swt.specific.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpResourceManager.java

index 3d58eb0c03e44550fa7b515efd4c9b5d63dc0ff0..330bc4fca94bd79b5de123e25dd24079678ad9c7 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -8,8 +8,8 @@ A2_CATEGORY = org.argeo.cms
 
 BUNDLES = \
 org.argeo.init \
-org.argeo.util \
 org.argeo.api.uuid \
+org.argeo.api.register \
 org.argeo.api.acr \
 org.argeo.api.cli \
 org.argeo.api.cms \
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ArgeoNamespace.java b/org.argeo.api.acr/src/org/argeo/api/acr/ArgeoNamespace.java
new file mode 100644 (file)
index 0000000..98131d1
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.api.acr;
+
+/** Namespaces declared by Argeo. */
+public enum ArgeoNamespace {
+       ;
+
+       public final static String CR_NAMESPACE_URI = "http://www.argeo.org/ns/cr";
+       public final static String CR_DEFAULT_PREFIX = "cr";
+       public final static String LDAP_NAMESPACE_URI = "http://www.argeo.org/ns/ldap";
+       public final static String LDAP_DEFAULT_PREFIX = "ldap";
+       public final static String ROLE_NAMESPACE_URI = "http://www.argeo.org/ns/role";
+       public final static String ROLE_DEFAULT_PREFIX = "role";
+
+}
index 55ad079ec16b1927c8bc214c877a000d0f281ef7..3e12fb1c87067fd188bfa34cac4c44794c9ec5d2 100644 (file)
@@ -28,7 +28,7 @@ public enum CrAttributeType {
        // we do not support short and float, like recent additions to Java
        // (e.g. optional primitives)
        DATE_TIME(Instant.class, W3C_XML_SCHEMA_NS_URI, "dateTime", new InstantFormatter()), //
-       UUID(UUID.class, CrName.CR_NAMESPACE_URI, "uuid", new UuidFormatter()), //
+       UUID(UUID.class, ArgeoNamespace.CR_NAMESPACE_URI, "uuid", new UuidFormatter()), //
        ANY_URI(URI.class, W3C_XML_SCHEMA_NS_URI, "anyUri", new UriFormatter()), //
        STRING(String.class, W3C_XML_SCHEMA_NS_URI, "string", new StringFormatter()), //
        ;
index f618335077eadbcd093273cb584ed1d9520449fc..ead47377bddff3eb5cd26d179636735d3f578bdc 100644 (file)
@@ -34,14 +34,7 @@ public enum CrName implements QNamed {
        //
        ;
 
-       public final static String CR_NAMESPACE_URI = "http://www.argeo.org/ns/cr";
-       public final static String CR_DEFAULT_PREFIX = "cr";
-
-       public final static String LDAP_NAMESPACE_URI = "http://www.argeo.org/ns/ldap";
-       public final static String LDAP_DEFAULT_PREFIX = "ldap";
-
-       public final static String ROLE_NAMESPACE_URI = "http://www.argeo.org/ns/role";
-       public final static String ROLE_DEFAULT_PREFIX = "role";
+       
 
 //     private final ContentName value;
 
@@ -55,12 +48,12 @@ public enum CrName implements QNamed {
 
        @Override
        public String getNamespace() {
-               return CR_NAMESPACE_URI;
+               return ArgeoNamespace.CR_NAMESPACE_URI;
        }
 
        @Override
        public String getDefaultPrefix() {
-               return CR_DEFAULT_PREFIX;
+               return ArgeoNamespace.CR_DEFAULT_PREFIX;
        }
 
 }
index 0941597d749b7887f7dc4227e76e71acd211979c..1c55156ee352b389c387c56f2ea30103dae2371c 100644 (file)
@@ -61,9 +61,9 @@ public class RuntimeNamespaceContext implements NamespaceContext {
                register(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, XSD_INSTANCE_DEFAULT_PREFIX);
 
                // Argeo specific
-               register(CrName.CR_NAMESPACE_URI, CrName.CR_DEFAULT_PREFIX);
-               register(CrName.LDAP_NAMESPACE_URI, CrName.LDAP_DEFAULT_PREFIX);
-               register(CrName.ROLE_NAMESPACE_URI, CrName.ROLE_DEFAULT_PREFIX);
+               register(ArgeoNamespace.CR_NAMESPACE_URI, ArgeoNamespace.CR_DEFAULT_PREFIX);
+               register(ArgeoNamespace.LDAP_NAMESPACE_URI, ArgeoNamespace.LDAP_DEFAULT_PREFIX);
+               register(ArgeoNamespace.ROLE_NAMESPACE_URI, ArgeoNamespace.ROLE_DEFAULT_PREFIX);
        }
 
        public static NamespaceContext getNamespaceContext() {
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/Distinguished.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/Distinguished.java
new file mode 100644 (file)
index 0000000..52556cf
--- /dev/null
@@ -0,0 +1,36 @@
+package org.argeo.api.acr.ldap;
+
+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;
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapAttrs.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapAttrs.java
new file mode 100644 (file)
index 0000000..b5af752
--- /dev/null
@@ -0,0 +1,369 @@
+package org.argeo.api.acr.ldap;
+
+import static org.argeo.api.acr.ArgeoNamespace.LDAP_DEFAULT_PREFIX;
+import static org.argeo.api.acr.ArgeoNamespace.LDAP_NAMESPACE_URI;
+
+import java.util.function.Supplier;
+
+import javax.xml.namespace.QName;
+
+import org.argeo.api.acr.ContentName;
+import org.argeo.api.acr.QNamed;
+import org.argeo.api.acr.RuntimeNamespaceContext;
+
+/**
+ * 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 QNamed, SpecifiedName, Supplier<String> {
+       /** */
+       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"),
+
+       // RFC 2307bis (partial)
+       /** */
+       uidNumber("1.3.6.1.1.1.1.0", "RFC 2307bis"),
+       /** */
+       gidNumber("1.3.6.1.1.1.1.1", "RFC 2307bis"),
+       /** */
+       homeDirectory("1.3.6.1.1.1.1.3", "RFC 2307bis"),
+       /** */
+       loginShell("1.3.6.1.1.1.1.4", "RFC 2307bis"),
+       /** */
+       memberUid("1.3.6.1.1.1.1.12", "RFC 2307bis"),
+
+       //
+       ;
+
+       public final static String DN = "dn";
+
+       private final String oid, spec;
+       private final QName value;
+
+       LdapAttrs(String oid, String spec) {
+               this.oid = oid;
+               this.spec = spec;
+               this.value = new ContentName(LDAP_NAMESPACE_URI, name());
+       }
+
+       public QName qName() {
+               return value;
+       }
+
+       @Override
+       public String getID() {
+               return oid;
+       }
+
+       @Override
+       public String getSpec() {
+               return spec;
+       }
+
+       @Deprecated
+       public String property() {
+               return get();
+       }
+
+       @Deprecated
+       public String qualified() {
+               return get();
+       }
+
+       @Override
+       public String get() {
+               return RuntimeNamespaceContext.getNamespaceContext().getPrefix(LDAP_NAMESPACE_URI) + ":" + name();
+       }
+
+       @Override
+       public final String toString() {
+               // must return the name
+               return name();
+       }
+
+       @Override
+       public String getNamespace() {
+               return LDAP_NAMESPACE_URI;
+       }
+
+       @Override
+       public String getDefaultPrefix() {
+               return LDAP_DEFAULT_PREFIX;
+       }
+
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapObjs.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/LdapObjs.java
new file mode 100644 (file)
index 0000000..45c8a54
--- /dev/null
@@ -0,0 +1,156 @@
+package org.argeo.api.acr.ldap;
+
+import static org.argeo.api.acr.ArgeoNamespace.LDAP_DEFAULT_PREFIX;
+import static org.argeo.api.acr.ArgeoNamespace.LDAP_NAMESPACE_URI;
+
+import java.util.function.Supplier;
+
+import javax.xml.namespace.QName;
+
+import org.argeo.api.acr.ArgeoNamespace;
+import org.argeo.api.acr.ContentName;
+import org.argeo.api.acr.QNamed;
+import org.argeo.api.acr.RuntimeNamespaceContext;
+
+/**
+ * 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 QNamed, SpecifiedName, Supplier<String> {
+       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"),
+
+       // RFC 2307bis (partial)
+       /** */
+       posixAccount("1.3.6.1.1.1.2.0", "RFC 2307bis"),
+       /** */
+       posixGroup("1.3.6.1.1.1.2.2", "RFC 2307bis"),
+
+       //
+       ;
+
+       private final String oid, spec;
+       private final QName value;
+
+       private LdapObjs(String oid, String spec) {
+               this.oid = oid;
+               this.spec = spec;
+               this.value = new ContentName(ArgeoNamespace.LDAP_NAMESPACE_URI, name());
+       }
+
+       public QName qName() {
+               return value;
+       }
+
+       public String getOid() {
+               return oid;
+       }
+
+       public String getSpec() {
+               return spec;
+       }
+
+       @Deprecated
+       public String property() {
+               return get();
+       }
+
+       @Override
+       public String get() {
+               return RuntimeNamespaceContext.getNamespaceContext().getPrefix(LDAP_NAMESPACE_URI) + ":" + name();
+       }
+
+       @Override
+       public String getNamespace() {
+               return LDAP_NAMESPACE_URI;
+       }
+
+       @Override
+       public String getDefaultPrefix() {
+               return LDAP_DEFAULT_PREFIX;
+       }
+
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/NamingUtils.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/NamingUtils.java
new file mode 100644 (file)
index 0000000..9f7ec61
--- /dev/null
@@ -0,0 +1,106 @@
+package org.argeo.api.acr.ldap;
+
+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");
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/NodeOID.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/NodeOID.java
new file mode 100644 (file)
index 0000000..a68b6cb
--- /dev/null
@@ -0,0 +1,17 @@
+package org.argeo.api.acr.ldap;
+
+interface NodeOID {
+       String BASE = "1.3.6.1.4.1" + ".48308" + ".1";
+
+       // uuidgen --md5 --namespace @oid --name 1.3.6.1.4.1.48308
+       String BASE_UUID_V3 = "6869e86b-96b7-3d49-b6ab-ffffc5ad95fb";
+       
+       // uuidgen --sha1 --namespace @oid --name 1.3.6.1.4.1.48308
+       String BASE_UUID_V5 = "58873947-460c-59a6-a7b4-28a97def5f27";
+       
+       // ATTRIBUTE TYPES
+       String ATTRIBUTE_TYPES = BASE + ".4";
+
+       // OBJECT CLASSES
+       String OBJECT_CLASSES = BASE + ".6";
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/ldap/SpecifiedName.java b/org.argeo.api.acr/src/org/argeo/api/acr/ldap/SpecifiedName.java
new file mode 100644 (file)
index 0000000..5de8ab2
--- /dev/null
@@ -0,0 +1,20 @@
+package org.argeo.api.acr.ldap;
+
+/**
+ * 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();
+       }
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/ArrayTabularRow.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/ArrayTabularRow.java
new file mode 100644 (file)
index 0000000..c0efe87
--- /dev/null
@@ -0,0 +1,25 @@
+package org.argeo.api.acr.tabular;
+
+import java.util.List;
+
+/** Minimal tabular row wrapping an {@link Object} array */
+public class ArrayTabularRow implements TabularRow {
+       private final Object[] arr;
+
+       public ArrayTabularRow(List<?> objs) {
+               this.arr = objs.toArray();
+       }
+
+       public Object get(Integer col) {
+               return arr[col];
+       }
+
+       public int size() {
+               return arr.length;
+       }
+
+       public Object[] toArray() {
+               return arr;
+       }
+
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularColumn.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularColumn.java
new file mode 100644 (file)
index 0000000..5b9bf23
--- /dev/null
@@ -0,0 +1,41 @@
+package org.argeo.api.acr.tabular;
+
+/** The column in a tabular content */
+public class TabularColumn {
+       private String name;
+       /**
+        * JCR types, see
+        * http://www.day.com/maven/javax.jcr/javadocs/jcr-2.0/index.html
+        * ?javax/jcr/PropertyType.html
+        */
+       private Integer type;
+
+       /** column with default type */
+       public TabularColumn(String name) {
+               super();
+               this.name = name;
+       }
+
+       public TabularColumn(String name, Integer type) {
+               super();
+               this.name = name;
+               this.type = type;
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public void setName(String name) {
+               this.name = name;
+       }
+
+       public Integer getType() {
+               return type;
+       }
+
+       public void setType(Integer type) {
+               this.type = type;
+       }
+
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularContent.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularContent.java
new file mode 100644 (file)
index 0000000..ae6eb4e
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.api.acr.tabular;
+
+import java.util.List;
+
+/**
+ * Content organized as a table, possibly with headers. Only JCR types are
+ * supported even though there is not direct dependency on JCR.
+ */
+public interface TabularContent {
+       /** The headers of this table or <code>null</code> is none available. */
+       public List<TabularColumn> getColumns();
+
+       public TabularRowIterator read();
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRow.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRow.java
new file mode 100644 (file)
index 0000000..5302cc0
--- /dev/null
@@ -0,0 +1,13 @@
+package org.argeo.api.acr.tabular;
+
+/** A row of tabular data */
+public interface TabularRow {
+       /** The value at this column index */
+       public Object get(Integer col);
+
+       /** The raw objects (direct references) */
+       public Object[] toArray();
+
+       /** Number of columns */
+       public int size();
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRowIterator.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularRowIterator.java
new file mode 100644 (file)
index 0000000..768c593
--- /dev/null
@@ -0,0 +1,12 @@
+package org.argeo.api.acr.tabular;
+
+import java.util.Iterator;
+
+/** Navigation of rows */
+public interface TabularRowIterator extends Iterator<TabularRow> {
+       /**
+        * Current row number, has to be incremented by each call to next() ; starts at 0, will
+        * therefore be 1 for the first row returned.
+        */
+       public Long getCurrentRowNumber();
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularWriter.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/TabularWriter.java
new file mode 100644 (file)
index 0000000..f1d555f
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.api.acr.tabular;
+
+
+/** Write to a tabular content */
+public interface TabularWriter {
+       /** Append a new row of data */
+       public void appendRow(Object[] row);
+
+       /** Finish persisting data and release resources */
+       public void close();
+}
diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/tabular/package-info.java b/org.argeo.api.acr/src/org/argeo/api/acr/tabular/package-info.java
new file mode 100644 (file)
index 0000000..06acbc5
--- /dev/null
@@ -0,0 +1,2 @@
+/** Tabular format API. */
+package org.argeo.api.acr.tabular;
\ No newline at end of file
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/directory/Directory.java b/org.argeo.api.cms/src/org/argeo/api/cms/directory/Directory.java
new file mode 100644 (file)
index 0000000..7ed61eb
--- /dev/null
@@ -0,0 +1,32 @@
+package org.argeo.api.cms.directory;
+
+import java.util.Optional;
+
+import org.argeo.api.cms.transaction.WorkControl;
+
+/** An information directory (typicylly LDAP). */
+public interface Directory extends HierarchyUnit {
+       String getName();
+
+       /** Whether this directory is read only. */
+       boolean isReadOnly();
+
+       /** Whether this directory is disabled. */
+       boolean isDisabled();
+
+       /** The realm (typically Kerberos) of this directory. */
+       Optional<String> getRealm();
+
+       /** Sets the transaction control used by this directory when editing. */
+       void setTransactionControl(WorkControl transactionControl);
+
+       /*
+        * HIERARCHY
+        */
+
+       /** The hierarchy unit at this path. */
+       HierarchyUnit getHierarchyUnit(String path);
+
+       /** Create a new hierarchy unit. */
+       HierarchyUnit createHierarchyUnit(String path);
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/directory/DirectoryDigestUtils.java b/org.argeo.api.cms/src/org/argeo/api/cms/directory/DirectoryDigestUtils.java
new file mode 100644 (file)
index 0000000..dabcfe8
--- /dev/null
@@ -0,0 +1,114 @@
+package org.argeo.api.cms.directory;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+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. */
+public class DirectoryDigestUtils {
+       public final static String PASSWORD_SCHEME_SHA = "SHA";
+       public final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256";
+
+       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 IllegalStateException("Cannot SHA1 digest", e);
+               }
+       }
+
+       public 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 (NoSuchAlgorithmException | InvalidKeySpecException e) {
+                       throw new IllegalStateException("Cannot digest", e);
+               }
+       }
+
+       public 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;
+       }
+
+       public 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;
+       }
+
+       public 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);
+       }
+
+       /** singleton */
+       private DirectoryDigestUtils() {
+       }
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/directory/HierarchyUnit.java b/org.argeo.api.cms/src/org/argeo/api/cms/directory/HierarchyUnit.java
new file mode 100644 (file)
index 0000000..04593d9
--- /dev/null
@@ -0,0 +1,42 @@
+package org.argeo.api.cms.directory;
+
+import java.util.Dictionary;
+import java.util.Locale;
+
+/** A unit within the high-level organisational structure of a directory. */
+public interface HierarchyUnit {
+       /** Name to use in paths. */
+       String getHierarchyUnitName();
+
+       /** Name to use in UI. */
+       String getHierarchyUnitLabel(Locale locale);
+
+       /**
+        * The parent {@link HierarchyUnit}, or <code>null</code> if a
+        * {@link Directory}.
+        */
+       HierarchyUnit getParent();
+
+       /** Direct children {@link HierarchyUnit}s. */
+       Iterable<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly);
+
+       /**
+        * Whether this is an arbitrary named and placed {@link HierarchyUnit}.
+        * 
+        * @return <code>true</code> if functional, <code>false</code> is technical
+        *         (e.g. People, Groups, etc.)
+        */
+       boolean isFunctional();
+
+       /**
+        * The base of this organisational unit within the hierarchy. This would
+        * typically be an LDAP base DN.
+        */
+       String getBase();
+
+       /** The related {@link Directory}. */
+       Directory getDirectory();
+
+       /** Its metadata (typically LDAP attributes). */
+       Dictionary<String, Object> getProperties();
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/keyring/CryptoKeyring.java b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/CryptoKeyring.java
new file mode 100644 (file)
index 0000000..454f8b4
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.api.cms.keyring;
+
+/**
+ * Marker interface for an advanced keyring based on cryptography.
+ */
+public interface CryptoKeyring extends Keyring {
+       public void changePassword(char[] oldPassword, char[] newPassword);
+
+       public void unlock(char[] password);
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/keyring/Keyring.java b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/Keyring.java
new file mode 100644 (file)
index 0000000..efc9455
--- /dev/null
@@ -0,0 +1,26 @@
+package org.argeo.api.cms.keyring;
+
+import java.io.InputStream;
+
+/**
+ * Access to private (typically encrypted) data. The keyring is responsible for
+ * retrieving the necessary credentials. <b>Experimental. This API may
+ * change.</b>
+ */
+public interface Keyring {
+       /**
+        * Returns the confidential information as chars. Must ask for it if it is
+        * not stored.
+        */
+       public char[] getAsChars(String path);
+
+       /**
+        * Returns the confidential information as a stream. Must ask for it if it
+        * is not stored.
+        */
+       public InputStream getAsStream(String path);
+
+       public void set(String path, char[] arr);
+
+       public void set(String path, InputStream in);
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/keyring/PBEKeySpecCallback.java b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/PBEKeySpecCallback.java
new file mode 100644 (file)
index 0000000..6a7ce19
--- /dev/null
@@ -0,0 +1,63 @@
+package org.argeo.api.cms.keyring;
+
+import javax.crypto.spec.PBEKeySpec;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.PasswordCallback;
+
+/**
+ * All information required to set up a {@link PBEKeySpec} bar the password
+ * itself (use a {@link PasswordCallback})
+ */
+public class PBEKeySpecCallback implements Callback {
+       private String secretKeyFactory;
+       private byte[] salt;
+       private Integer iterationCount;
+       /** Can be null for some algorithms */
+       private Integer keyLength;
+       /** Can be null, will trigger secret key encryption if not */
+       private String secretKeyEncryption;
+
+       private String encryptedPasswordHashCipher;
+       private byte[] encryptedPasswordHash;
+
+       public void set(String secretKeyFactory, byte[] salt,
+                       Integer iterationCount, Integer keyLength,
+                       String secretKeyEncryption) {
+               this.secretKeyFactory = secretKeyFactory;
+               this.salt = salt;
+               this.iterationCount = iterationCount;
+               this.keyLength = keyLength;
+               this.secretKeyEncryption = secretKeyEncryption;
+//             this.encryptedPasswordHashCipher = encryptedPasswordHashCipher;
+//             this.encryptedPasswordHash = encryptedPasswordHash;
+       }
+
+       public String getSecretKeyFactory() {
+               return secretKeyFactory;
+       }
+
+       public byte[] getSalt() {
+               return salt;
+       }
+
+       public Integer getIterationCount() {
+               return iterationCount;
+       }
+
+       public Integer getKeyLength() {
+               return keyLength;
+       }
+
+       public String getSecretKeyEncryption() {
+               return secretKeyEncryption;
+       }
+
+       public String getEncryptedPasswordHashCipher() {
+               return encryptedPasswordHashCipher;
+       }
+
+       public byte[] getEncryptedPasswordHash() {
+               return encryptedPasswordHash;
+       }
+
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/keyring/package-info.java b/org.argeo.api.cms/src/org/argeo/api/cms/keyring/package-info.java
new file mode 100644 (file)
index 0000000..7f61e09
--- /dev/null
@@ -0,0 +1,2 @@
+/** Argeo CMS reusable security components. */
+package org.argeo.api.cms.keyring;
\ No newline at end of file
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/AbstractWorkingCopy.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/AbstractWorkingCopy.java
new file mode 100644 (file)
index 0000000..928acad
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.api.cms.transaction;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class AbstractWorkingCopy<DATA, ATTR, ID> implements WorkingCopy<DATA, ATTR, ID> {
+       private Map<ID, DATA> newData = new HashMap<ID, DATA>();
+       private Map<ID, ATTR> modifiedData = new HashMap<ID, ATTR>();
+       private Map<ID, DATA> deletedData = new HashMap<ID, DATA>();
+
+       protected abstract ID getId(DATA data);
+
+       protected abstract ATTR cloneAttributes(DATA data);
+
+       public void cleanUp() {
+               // clean collections
+               newData.clear();
+               newData = null;
+               modifiedData.clear();
+               modifiedData = null;
+               deletedData.clear();
+               deletedData = null;
+       }
+
+       public boolean noModifications() {
+               return newData.size() == 0 && modifiedData.size() == 0 && deletedData.size() == 0;
+       }
+
+       public void startEditing(DATA user) {
+               ID id = getId(user);
+               if (modifiedData.containsKey(id))
+                       throw new IllegalStateException("Already editing " + id);
+               modifiedData.put(id, cloneAttributes(user));
+       }
+
+       public Map<ID, DATA> getNewData() {
+               return newData;
+       }
+
+       public Map<ID, DATA> getDeletedData() {
+               return deletedData;
+       }
+
+       public Map<ID, ATTR> getModifiedData() {
+               return modifiedData;
+       }
+
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/JtaStatusAdapter.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/JtaStatusAdapter.java
new file mode 100644 (file)
index 0000000..2ba6c0d
--- /dev/null
@@ -0,0 +1,61 @@
+package org.argeo.api.cms.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;
+       }
+
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleRollbackException.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleRollbackException.java
new file mode 100644 (file)
index 0000000..39ed9b9
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.api.cms.transaction;
+
+/** Internal unchecked rollback exception. */
+class SimpleRollbackException extends RuntimeException {
+       private static final long serialVersionUID = 8055601819719780566L;
+
+       public SimpleRollbackException() {
+               super();
+       }
+
+       public SimpleRollbackException(Throwable cause) {
+               super(cause);
+       }
+
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransaction.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransaction.java
new file mode 100644 (file)
index 0000000..f2bb907
--- /dev/null
@@ -0,0 +1,160 @@
+package org.argeo.api.cms.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;
+       }
+
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransactionManager.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/SimpleTransactionManager.java
new file mode 100644 (file)
index 0000000..ee99ccb
--- /dev/null
@@ -0,0 +1,214 @@
+package org.argeo.api.cms.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);
+//                     }
+//             }
+//
+//     }
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/TransactionStatusAdapter.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/TransactionStatusAdapter.java
new file mode 100644 (file)
index 0000000..ab4effd
--- /dev/null
@@ -0,0 +1,22 @@
+package org.argeo.api.cms.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();
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/UuidXid.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/UuidXid.java
new file mode 100644 (file)
index 0000000..83358a5
--- /dev/null
@@ -0,0 +1,132 @@
+package org.argeo.api.cms.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);
+       // }
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkContext.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkContext.java
new file mode 100644 (file)
index 0000000..5493dde
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.api.cms.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);
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkControl.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkControl.java
new file mode 100644 (file)
index 0000000..de03150
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.api.cms.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();
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkTransaction.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkTransaction.java
new file mode 100644 (file)
index 0000000..39c188d
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.api.cms.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();
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopy.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopy.java
new file mode 100644 (file)
index 0000000..c79423c
--- /dev/null
@@ -0,0 +1,18 @@
+package org.argeo.api.cms.transaction;
+
+import java.util.Map;
+
+public interface WorkingCopy<DATA, ATTR, ID> {
+       void startEditing(DATA user);
+
+       boolean noModifications();
+
+       void cleanUp();
+
+       Map<ID, DATA> getNewData();
+
+       Map<ID, DATA> getDeletedData();
+
+       Map<ID, ATTR> getModifiedData();
+
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyProcessor.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyProcessor.java
new file mode 100644 (file)
index 0000000..9e7c9e1
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.api.cms.transaction;
+
+public interface WorkingCopyProcessor<WC extends WorkingCopy<?, ?, ?>> {
+       void prepare(WC wc);
+
+       void commit(WC wc);
+
+       void rollback(WC wc);
+       
+       WC newWorkingCopy();
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyXaResource.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/WorkingCopyXaResource.java
new file mode 100644 (file)
index 0000000..16b08c2
--- /dev/null
@@ -0,0 +1,138 @@
+package org.argeo.api.cms.transaction;
+
+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. */
+public class WorkingCopyXaResource<WC extends WorkingCopy<?, ?, ?>> implements XAResource {
+       private final WorkingCopyProcessor<WC> processor;
+
+       private Map<Xid, WC> workingCopies = new HashMap<Xid, WC>();
+       private Xid editingXid = null;
+       private int transactionTimeout = 0;
+
+       public WorkingCopyXaResource(WorkingCopyProcessor<WC> processor) {
+               this.processor = processor;
+       }
+
+       @Override
+       public synchronized void start(Xid xid, int flags) throws XAException {
+               if (editingXid != null)
+                       throw new IllegalStateException("Already editing " + editingXid);
+               WC wc = workingCopies.put(xid, processor.newWorkingCopy());
+               if (wc != null)
+                       throw new IllegalStateException("There is already a working copy for " + xid);
+               this.editingXid = xid;
+       }
+
+       @Override
+       public void end(Xid xid, int flags) throws XAException {
+               checkXid(xid);
+       }
+
+       private WC wc(Xid xid) {
+               return workingCopies.get(xid);
+       }
+
+       public synchronized WC wc() {
+               if (editingXid == null)
+                       return null;
+               WC wc = workingCopies.get(editingXid);
+               if (wc == null)
+                       throw new IllegalStateException("No working copy found for " + editingXid);
+               return wc;
+       }
+
+       private synchronized void cleanUp(Xid xid) {
+               WC wc = workingCopies.get(xid);
+               if (wc != null) {
+                       wc.cleanUp();
+                       workingCopies.remove(xid);
+               }
+               editingXid = null;
+       }
+
+       @Override
+       public int prepare(Xid xid) throws XAException {
+               checkXid(xid);
+               WC wc = wc(xid);
+               if (wc.noModifications())
+                       return XA_RDONLY;
+               try {
+                       processor.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);
+                       WC wc = wc(xid);
+                       if (wc.noModifications())
+                               return;
+                       if (onePhase)
+                               processor.prepare(wc);
+                       processor.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);
+                       processor.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);
+       }
+
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/XAResourceProvider.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/XAResourceProvider.java
new file mode 100644 (file)
index 0000000..904fb5f
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.api.cms.transaction;
+
+import javax.transaction.xa.XAResource;
+
+public interface XAResourceProvider {
+       XAResource getXaResource();
+}
diff --git a/org.argeo.api.cms/src/org/argeo/api/cms/transaction/package-info.java b/org.argeo.api.cms/src/org/argeo/api/cms/transaction/package-info.java
new file mode 100644 (file)
index 0000000..bbb9212
--- /dev/null
@@ -0,0 +1,2 @@
+/** Minimalistic and partial XA transaction manager implementation. */
+package org.argeo.api.cms.transaction;
\ No newline at end of file
diff --git a/org.argeo.api.register/.classpath b/org.argeo.api.register/.classpath
new file mode 100644 (file)
index 0000000..4199cd3
--- /dev/null
@@ -0,0 +1,11 @@
+<?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-17">
+               <attributes>
+                       <attribute name="module" value="true"/>
+               </attributes>
+       </classpathentry>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.api.register/.project b/org.argeo.api.register/.project
new file mode 100644 (file)
index 0000000..a3164c1
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.api.register</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>
diff --git a/org.argeo.api.register/META-INF/.gitignore b/org.argeo.api.register/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.api.register/bnd.bnd b/org.argeo.api.register/bnd.bnd
new file mode 100644 (file)
index 0000000..bcebf37
--- /dev/null
@@ -0,0 +1,3 @@
+Import-Package:        org.osgi.*;version=0.0.0,\
+!org.apache.commons.logging,\
+*                              
diff --git a/org.argeo.api.register/build.properties b/org.argeo.api.register/build.properties
new file mode 100644 (file)
index 0000000..ae2abc5
--- /dev/null
@@ -0,0 +1 @@
+source.. = src/
\ No newline at end of file
diff --git a/org.argeo.api.register/src/org/argeo/api/register/Component.java b/org.argeo.api.register/src/org/argeo/api/register/Component.java
new file mode 100644 (file)
index 0000000..f4676d9
--- /dev/null
@@ -0,0 +1,333 @@
+package org.argeo.api.register;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * A wrapper for an object, whose dependencies and life cycle can be managed.
+ */
+public class Component<I> implements Supplier<I>, Comparable<Component<?>> {
+
+       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 final Map<String, Object> properties;
+
+       private CompletableFuture<Void> activationStarted = null;
+       private CompletableFuture<Void> activated = null;
+
+       private CompletableFuture<Void> deactivationStarted = null;
+       private CompletableFuture<Void> deactivated = null;
+
+       // internal
+       private Set<Dependency<?>> dependants = new HashSet<>();
+
+       private RankingKey rankingKey;
+
+       Component(ComponentRegister register, I instance, Runnable init, Runnable close, Set<Dependency<?>> dependencies,
+                       Set<Class<? super I>> classes, Map<String, Object> properties) {
+               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();
+
+               long serviceId = register.register(this);
+               Map<String, Object> props = new HashMap<>(properties);
+               props.put(RankingKey.SERVICE_ID, serviceId);
+               this.properties = Collections.unmodifiableMap(props);
+               rankingKey = new RankingKey(properties);
+       }
+
+       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());
+       }
+
+       CompletableFuture<Void> getActivated() {
+               return activated;
+       }
+
+       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);
+       }
+
+       @Override
+       public I get() {
+               return instance;
+       }
+
+       @SuppressWarnings("unchecked")
+       public <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);
+       }
+
+       public <T> boolean isPublishedType(Class<T> clss) {
+               return types.containsKey(clss);
+       }
+
+       public Map<String, Object> getProperties() {
+               return properties;
+       }
+
+       @Override
+       public int compareTo(Component<?> o) {
+               return rankingKey.compareTo(rankingKey);
+       }
+
+       @Override
+       public int hashCode() {
+               Long serviceId = (Long) properties.get(RankingKey.SERVICE_ID);
+               if (serviceId != null)
+                       return serviceId.intValue();
+               else
+                       return super.hashCode();
+       }
+
+       @Override
+       public String toString() {
+               List<String> classes = new ArrayList<>();
+               for (Class<?> clss : types.keySet()) {
+                       classes.add(clss.getName());
+               }
+               return "Component " + classes + " " + properties + "";
+       }
+
+       /** A type which has been explicitly exposed by a component. */
+       public static class PublishedType<T> {
+               private Component<? extends T> component;
+               private Class<T> clss;
+
+               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);
+               }
+
+               public Component<?> getPublisher() {
+                       return component;
+               }
+
+               public Class<T> getType() {
+                       return clss;
+               }
+
+               public CompletionStage<T> getValue() {
+                       return value.minimalCompletionStage();
+               }
+       }
+
+       /** Builds a {@link Component}. */
+       public static class Builder<I> implements Supplier<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<>();
+               private final Map<String, Object> properties = new HashMap<>();
+
+               public Builder(I instance) {
+                       this.instance = instance;
+               }
+
+               public Component<I> build(ComponentRegister 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, properties);
+                       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> addActivation(Runnable init) {
+                       if (this.init != null)
+                               throw new IllegalArgumentException("init method is already set");
+                       this.init = init;
+                       return this;
+               }
+
+               public Builder<I> addDeactivation(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 void addProperty(String key, Object value) {
+                       if (properties.containsKey(key))
+                               throw new IllegalStateException("Key " + key + " is already set.");
+                       properties.put(key, value);
+               }
+
+               @Override
+               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 != null ? set : t -> {
+                       };
+                       this.unset = unset != null ? unset : t -> {
+                       };
+               }
+
+               // 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);
+               }
+
+       }
+}
diff --git a/org.argeo.api.register/src/org/argeo/api/register/ComponentRegister.java b/org.argeo.api.register/src/org/argeo/api/register/ComponentRegister.java
new file mode 100644 (file)
index 0000000..1bb9036
--- /dev/null
@@ -0,0 +1,43 @@
+package org.argeo.api.register;
+
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.function.Predicate;
+
+/** A register of components which can coordinate their activation. */
+public interface ComponentRegister {
+       long register(Component<?> component);
+
+       <T> SortedSet<Component<? extends T>> find(Class<T> clss, Predicate<Map<String, Object>> filter);
+
+       default <T> Component.PublishedType<T> getSingleton(Class<T> type) {
+               SortedSet<Component<? extends T>> found = find(type, null);
+               if (found.size() == 0)
+                       throw new IllegalStateException("No component found for " + type);
+               return found.first().getType(type);
+       }
+
+       default <T> T getObject(Class<T> clss) {
+               SortedSet<Component<? extends T>> found = find(clss, null);
+               if (found.size() == 0)
+                       return null;
+               return found.first().get();
+       }
+
+       Component<?> get(Object instance);
+
+//     default <T> PublishedType<T> getType(Class<T> clss) {
+//             SortedSet<Component<? extends T>> components = find(clss, null);
+//             if (components.size() == 0)
+//                     return null;
+//             return components.first().getType(clss);
+//     }
+
+       void activate();
+
+       void deactivate();
+
+       boolean isActive();
+
+       void clear();
+}
diff --git a/org.argeo.api.register/src/org/argeo/api/register/RankingKey.java b/org.argeo.api.register/src/org/argeo/api/register/RankingKey.java
new file mode 100644 (file)
index 0000000..3886afe
--- /dev/null
@@ -0,0 +1,105 @@
+package org.argeo.api.register;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Key used to classify and filter available components.
+ */
+public class RankingKey implements Comparable<RankingKey> {
+       public final static String SERVICE_PID = "service.pid";
+       public final static String SERVICE_ID = "service.id";
+       public final static String SERVICE_RANKING = "service.ranking";
+
+       private String pid;
+       private Integer ranking = 0;
+       private Long id = 0l;
+
+       public RankingKey(String pid, Integer ranking, Long id) {
+               super();
+               this.pid = pid;
+               this.ranking = ranking;
+               this.id = id;
+       }
+
+       public RankingKey(Map<String, Object> properties) {
+               this.pid = properties.containsKey(SERVICE_PID) ? properties.get(SERVICE_PID).toString() : null;
+               this.ranking = properties.containsKey(SERVICE_RANKING)
+                               ? Integer.parseInt(properties.get(SERVICE_RANKING).toString())
+                               : 0;
+               this.id = properties.containsKey(SERVICE_ID) ? (Long) properties.get(SERVICE_ID) : null;
+       }
+
+       @Override
+       public int hashCode() {
+               Integer result = 0;
+               if (pid != null)
+                       result = +pid.hashCode();
+               if (ranking != null)
+                       result = +ranking;
+               return result;
+       }
+
+       @Override
+       protected Object clone() throws CloneNotSupportedException {
+               return new RankingKey(pid, ranking, id);
+       }
+
+       @Override
+       public String toString() {
+               StringBuilder sb = new StringBuilder("");
+               if (pid != null)
+                       sb.append(pid);
+               if (ranking != null && ranking != 0)
+                       sb.append(' ').append(ranking);
+               return sb.toString();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (!(obj instanceof RankingKey))
+                       return false;
+               RankingKey other = (RankingKey) obj;
+               return Objects.equals(pid, other.pid) && Objects.equals(ranking, other.ranking) && Objects.equals(id, other.id);
+       }
+
+       @Override
+       public int compareTo(RankingKey o) {
+               if (pid != null && o.pid != null) {
+                       if (pid.equals(o.pid)) {
+                               if (ranking.equals(o.ranking))
+                                       if (id != null && o.id != null)
+                                               return id.compareTo(o.id);
+                                       else
+                                               return 0;
+                               else
+                                       return ranking.compareTo(o.ranking);
+                       } else {
+                               return pid.compareTo(o.pid);
+                       }
+
+               } else {
+               }
+               return -1;
+       }
+
+       public String getPid() {
+               return pid;
+       }
+
+       public Integer getRanking() {
+               return ranking;
+       }
+
+       public Long getId() {
+               return id;
+       }
+
+       public static RankingKey minPid(String pid) {
+               return new RankingKey(pid, Integer.MIN_VALUE, null);
+       }
+
+       public static RankingKey maxPid(String pid) {
+               return new RankingKey(pid, Integer.MAX_VALUE, null);
+       }
+}
diff --git a/org.argeo.api.register/src/org/argeo/api/register/SimpleRegister.java b/org.argeo.api.register/src/org/argeo/api/register/SimpleRegister.java
new file mode 100644 (file)
index 0000000..9ed7e76
--- /dev/null
@@ -0,0 +1,124 @@
+package org.argeo.api.register;
+
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Predicate;
+
+/** A minimal component register. */
+public class SimpleRegister implements ComponentRegister {
+       private final AtomicBoolean started = new AtomicBoolean(false);
+       private final IdentityHashMap<Object, Component<?>> components = new IdentityHashMap<>();
+       private final AtomicLong nextServiceId = new AtomicLong(0l);
+
+       @Override
+       public long register(Component<?> component) {
+               return registerComponent(component);
+       }
+
+       @SuppressWarnings({ "unchecked" })
+       @Override
+       public synchronized <T> SortedSet<Component<? extends T>> find(Class<T> clss,
+                       Predicate<Map<String, Object>> filter) {
+               SortedSet<Component<? extends T>> result = new TreeSet<>();
+               instances: for (Object instance : components.keySet()) {
+                       if (!clss.isAssignableFrom(instance.getClass()))
+                               continue instances;
+                       Component<? extends T> component = (Component<? extends T>) components.get(instance);
+
+                       if (component.isPublishedType(clss)) {
+                               if (filter != null) {
+                                       filter.test(component.getProperties());
+                               }
+                               result.add(component);
+                       }
+               }
+               if (result.isEmpty())
+                       return null;
+               return result;
+
+       }
+
+       synchronized long registerComponent(Component<?> component) {
+               if (started.get()) // TODO make it really dynamic
+                       throw new IllegalStateException("Already activated");
+               if (components.containsKey(component.get()))
+                       throw new IllegalArgumentException("Already registered as component");
+               components.put(component.get(), component);
+               return nextServiceId.incrementAndGet();
+       }
+
+       @Override
+       public synchronized Component<?> get(Object instance) {
+               if (!components.containsKey(instance))
+                       throw new IllegalArgumentException("Not registered as component");
+               return components.get(instance);
+       }
+
+       @Override
+       public synchronized 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 (InterruptedException e) {
+                       throw new RuntimeException("Register activation has been interrupted", e);
+               } catch (ExecutionException e) {
+                       if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) {
+                               throw (RuntimeException) e.getCause();
+                       } else {
+                               throw new IllegalStateException("Cannot activate register", e.getCause());
+                       }
+               }
+       }
+
+       @Override
+       public synchronized 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 (InterruptedException e) {
+                       throw new RuntimeException("Register deactivation has been interrupted", e);
+               } catch (ExecutionException e) {
+                       if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) {
+                               throw (RuntimeException) e.getCause();
+                       } else {
+                               throw new IllegalStateException("Cannot deactivate register", e.getCause());
+                       }
+               }
+       }
+
+       @Override
+       public synchronized boolean isActive() {
+               return started.get();
+       }
+
+       @Override
+       public synchronized void clear() {
+               components.clear();
+       }
+}
index 08ce1365c513203b391e2a3736acd727fa3091ea..f09995c002a580a5684daf890e871e04148b319d 100644 (file)
@@ -1,5 +1,4 @@
 Import-Package:\
-org.argeo.util.http,\
 org.osgi.service.http;version=0.0.0,\
 org.osgi.service.http.whiteboard;version=0.0.0,\
 org.osgi.framework.namespace;version=0.0.0,\
index 205699464f43aab954127acb2cb82faa211f676d..672722946327febd0cf8b1c617b0a25c66b0bac0 100644 (file)
@@ -6,7 +6,7 @@ import java.io.Writer;
 import javax.servlet.http.HttpServletResponse;
 
 import org.argeo.api.cms.CmsLog;
-import org.argeo.util.ExceptionsChain;
+import org.argeo.cms.util.ExceptionsChain;
 
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.core.JsonProcessingException;
index 983202ad2147b7d30908a3ae09a4cc8cd6ed35d4..e170609bb6a2bed41cd660b2d8c5c807b3447e62 100644 (file)
@@ -15,13 +15,13 @@ import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.argeo.api.acr.ldap.NamingUtils;
 import org.argeo.api.cms.CmsAuth;
 import org.argeo.cms.CmsUserManager;
 import org.argeo.cms.auth.RemoteAuthCallback;
 import org.argeo.cms.auth.RemoteAuthCallbackHandler;
 import org.argeo.cms.servlet.ServletHttpRequest;
 import org.argeo.cms.servlet.ServletHttpResponse;
-import org.argeo.util.naming.NamingUtils;
 import org.osgi.service.useradmin.Authorization;
 
 import com.fasterxml.jackson.core.JsonGenerator;
index 6a5208730fdd4af179afbc1de536367629365b28..d3c0eb5402d45b5e25bb4b0bfe62d4b782f27d5b 100644 (file)
@@ -2,11 +2,8 @@ package org.argeo.cms.servlet;
 
 import java.io.IOException;
 import java.net.URL;
-import java.net.http.HttpHeaders;
-import java.security.PrivilegedAction;
 import java.util.Map;
 
-import javax.security.auth.Subject;
 import javax.security.auth.login.LoginContext;
 import javax.security.auth.login.LoginException;
 import javax.servlet.http.HttpServletRequest;
@@ -19,7 +16,6 @@ import org.argeo.cms.auth.RemoteAuthRequest;
 import org.argeo.cms.auth.RemoteAuthResponse;
 import org.argeo.cms.auth.RemoteAuthUtils;
 import org.argeo.cms.servlet.internal.HttpUtils;
-import org.argeo.util.http.HttpHeader;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.FrameworkUtil;
 import org.osgi.service.http.context.ServletContextHelper;
index dd0b70afffba25c709bf39dd19be135a0e43d79b..2b2ffcb10f084ff397a38df47354a72bb7dc32cd 100644 (file)
@@ -12,9 +12,9 @@ import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.argeo.cms.osgi.FilterRequirement;
 import org.argeo.cms.osgi.PublishNamespace;
-import org.argeo.osgi.util.FilterRequirement;
-import org.argeo.util.StreamUtils;
+import org.argeo.cms.util.StreamUtils;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.FrameworkUtil;
index cc608136b47c0ac965fd3a097266862f9bf6e315..f0c7fca3ac2b6c213ec2dbbb6dd79c214ee90972 100644 (file)
@@ -16,9 +16,9 @@ import javax.websocket.Session;
 import javax.websocket.server.PathParam;
 import javax.websocket.server.ServerEndpoint;
 
+import org.argeo.api.acr.ldap.NamingUtils;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.cms.integration.CmsExceptionsChain;
-import org.argeo.util.naming.NamingUtils;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.FrameworkUtil;
 import org.osgi.framework.ServiceRegistration;
index d393e4b2b28b402ffbe2af1c5dcb514c8b691c7a..4141cd8ccdb8698690b604f2318ab8dfb7e09031 100644 (file)
@@ -13,7 +13,7 @@ import javax.websocket.server.ServerContainer;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.api.cms.CmsState;
 import org.argeo.cms.CmsDeployProperty;
-import org.argeo.util.http.HttpServerUtils;
+import org.argeo.cms.http.HttpServerUtils;
 import org.eclipse.jetty.http.UriCompliance;
 import org.eclipse.jetty.server.HttpConfiguration;
 import org.eclipse.jetty.server.HttpConnectionFactory;
index be41f9cbd9f9bb83df80c70c62d5383884d23dfe..cde6c935ae2aff8e4b8ab135ea2169dfdd7224da 100644 (file)
@@ -9,7 +9,7 @@ import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.config.keys.DefaultAuthorizedKeysAuthenticator;
 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
 import org.apache.sshd.server.shell.ProcessShellFactory;
-import org.argeo.util.OS;
+import org.argeo.cms.util.OS;
 
 /** A simple SSH server with some defaults. Supports SCP. */
 public class BasicSshServer {
index 660db5967890f86051e03d47e3497dcc6accfa13..31e63411ad3333202e5b09ea495ace0301ec496a 100644 (file)
@@ -24,7 +24,7 @@ import org.apache.sshd.common.config.keys.FilePasswordProvider;
 import org.apache.sshd.sftp.client.fs.SftpFileSystem;
 import org.apache.sshd.sftp.client.fs.SftpFileSystemProvider;
 import org.argeo.api.cms.CmsLog;
-import org.argeo.util.StreamUtils;
+import org.argeo.cms.util.StreamUtils;
 
 public class SshSync {
        private final static CmsLog log = CmsLog.getLog(SshSync.class);
index 916cc74f2feb4c8bef5f9d3cb2be460e3f549d17..087b4ff7c725116eb8fe4f8be1b5536d9bb23681 100644 (file)
@@ -5,7 +5,7 @@ import org.argeo.api.acr.ContentRepository;
 import org.argeo.api.acr.ContentSession;
 import org.argeo.api.cms.ux.Cms2DSize;
 import org.argeo.api.cms.ux.CmsView;
-import org.argeo.util.CurrentSubject;
+import org.argeo.cms.util.CurrentSubject;
 
 public class CmsUxUtils {
        public static ContentSession getContentSession(ContentRepository contentRepository, CmsView cmsView) {
index 50a9ea6eb791c1f399d6948fc66ee6edaa4cff85..52c75318e044ed5a4ef713b0dbb0a6adfedd75b1 100644 (file)
@@ -2,8 +2,8 @@
 <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" activate="start" deactivate="stop" immediate="false" name="Node User Admin">
    <implementation class="org.argeo.cms.internal.runtime.CmsUserAdmin"/>
    <property name="service.pid" type="String" value="org.argeo.api.userAdmin"/>
-   <reference bind="setTransactionManager" cardinality="1..1" interface="org.argeo.util.transaction.WorkControl" name="WorkControl" policy="static"/>
-   <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.util.transaction.WorkTransaction" name="WorkTransaction" policy="static"/>
+   <reference bind="setTransactionManager" cardinality="1..1" interface="org.argeo.api.cms.transaction.WorkControl" name="WorkControl" policy="static"/>
+   <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.api.cms.transaction.WorkTransaction" name="WorkTransaction" policy="static"/>
  <reference bind="setCmsState" cardinality="1..1" interface="org.argeo.api.cms.CmsState" name="CmsState" policy="static"/>
  <service>
     <provide interface="org.osgi.service.useradmin.UserAdmin"/>
index d22660c9fc57034a6bb9345e6eee6b65fe428f52..1f0033171a68ef45b270b66cb6bf998e591d7e2a 100644 (file)
@@ -5,5 +5,5 @@
       <provide interface="org.argeo.cms.CmsUserManager"/>
    </service>
    <reference bind="setUserAdmin" cardinality="1..1" interface="org.osgi.service.useradmin.UserAdmin" name="UserAdmin" policy="static"/>
-   <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.util.transaction.WorkTransaction" name="UserTransaction" policy="static"/>
+   <reference bind="setUserTransaction" cardinality="1..1" interface="org.argeo.api.cms.transaction.WorkTransaction" name="UserTransaction" policy="static"/>
 </scr:component>
index 81997476eece13220a0333f669dfc7b1d451fa21..df317e937f97b1ed24cf1cc3ed5492410edf7950 100644 (file)
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="Simple Transaction Manager">
-   <implementation class="org.argeo.util.transaction.SimpleTransactionManager"/>
+   <implementation class="org.argeo.api.cms.transaction.SimpleTransactionManager"/>
    <service>
-      <provide interface="org.argeo.util.transaction.WorkControl"/>
-      <provide interface="org.argeo.util.transaction.WorkTransaction"/>
+      <provide interface="org.argeo.api.cms.transaction.WorkControl"/>
+      <provide interface="org.argeo.api.cms.transaction.WorkTransaction"/>
    </service>
 </scr:component>
diff --git a/org.argeo.cms/src/org/argeo/cms/AbstractKeyring.java b/org.argeo.cms/src/org/argeo/cms/AbstractKeyring.java
new file mode 100644 (file)
index 0000000..c2c9c61
--- /dev/null
@@ -0,0 +1,290 @@
+package org.argeo.cms;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.security.Provider;
+import java.security.Security;
+import java.util.Arrays;
+import java.util.Iterator;
+
+import javax.crypto.SecretKey;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.TextOutputCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.argeo.api.cms.CmsAuth;
+import org.argeo.api.cms.keyring.PBEKeySpecCallback;
+import org.argeo.cms.util.CurrentSubject;
+import org.argeo.cms.util.StreamUtils;
+import org.argeo.api.cms.keyring.CryptoKeyring;
+import org.argeo.api.cms.keyring.Keyring;
+
+/** username / password based keyring. TODO internationalize */
+public abstract class AbstractKeyring implements Keyring, CryptoKeyring {
+       // public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING";
+
+       // private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT;
+       private CallbackHandler defaultCallbackHandler;
+
+       private String charset = "UTF-8";
+
+       /**
+        * Default provider is bouncy castle, in order to have consistent behaviour
+        * across implementations
+        */
+       private String securityProviderName = "BC";
+
+       /**
+        * Whether the keyring has already been created in the past with a master
+        * password
+        */
+       protected abstract Boolean isSetup();
+
+       /**
+        * Setup the keyring persistently, {@link #isSetup()} must return true
+        * afterwards
+        */
+       protected abstract void setup(char[] password);
+
+       /** Populates the key spec callback */
+       protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback);
+
+       protected abstract void encrypt(String path, InputStream unencrypted);
+
+       protected abstract InputStream decrypt(String path);
+
+       /** Triggers lazy initialization */
+       protected SecretKey getSecretKey(char[] password) {
+               Subject subject = CurrentSubject.current();
+               if (subject == null)
+                       throw new IllegalStateException("Current subject cannot be null");
+               // we assume only one secrete key is available
+               Iterator<SecretKey> iterator = subject.getPrivateCredentials(SecretKey.class).iterator();
+               if (!iterator.hasNext() || password != null) {// not initialized
+                       CallbackHandler callbackHandler = password == null ? new KeyringCallbackHandler()
+                                       : new PasswordProvidedCallBackHandler(password);
+                       ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
+                       Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+                       try {
+                               LoginContext loginContext = new LoginContext(CmsAuth.LOGIN_CONTEXT_KEYRING, subject, callbackHandler);
+                               loginContext.login();
+                               // FIXME will login even if password is wrong
+                               iterator = subject.getPrivateCredentials(SecretKey.class).iterator();
+                               return iterator.next();
+                       } catch (LoginException e) {
+                               throw new IllegalStateException("Keyring login failed", e);
+                       } finally {
+                               Thread.currentThread().setContextClassLoader(currentContextClassLoader);
+                       }
+
+               } else {
+                       SecretKey secretKey = iterator.next();
+                       if (iterator.hasNext())
+                               throw new IllegalStateException("More than one secret key in private credentials");
+                       return secretKey;
+               }
+       }
+
+       public InputStream getAsStream(String path) {
+               return decrypt(path);
+       }
+
+       public void set(String path, InputStream in) {
+               encrypt(path, in);
+       }
+
+       public char[] getAsChars(String path) {
+               // InputStream in = getAsStream(path);
+               // CharArrayWriter writer = null;
+               // Reader reader = null;
+               try (InputStream in = getAsStream(path);
+                               CharArrayWriter writer = new CharArrayWriter();
+                               Reader reader = new InputStreamReader(in, charset);) {
+                       StreamUtils.copy(reader, writer);
+                       return writer.toCharArray();
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot decrypt to char array", e);
+               } finally {
+                       // IOUtils.closeQuietly(reader);
+                       // IOUtils.closeQuietly(in);
+                       // IOUtils.closeQuietly(writer);
+               }
+       }
+
+       public void set(String path, char[] arr) {
+               // ByteArrayOutputStream out = new ByteArrayOutputStream();
+               // ByteArrayInputStream in = null;
+               // Writer writer = null;
+               try (ByteArrayOutputStream out = new ByteArrayOutputStream();
+                               Writer writer = new OutputStreamWriter(out, charset);) {
+                       // writer = new OutputStreamWriter(out, charset);
+                       writer.write(arr);
+                       writer.flush();
+                       // in = new ByteArrayInputStream(out.toByteArray());
+                       try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());) {
+                               set(path, in);
+                       }
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot encrypt to char array", e);
+               } finally {
+                       // IOUtils.closeQuietly(writer);
+                       // IOUtils.closeQuietly(out);
+                       // IOUtils.closeQuietly(in);
+               }
+       }
+
+       public void unlock(char[] password) {
+               if (!isSetup())
+                       setup(password);
+               SecretKey secretKey = getSecretKey(password);
+               if (secretKey == null)
+                       throw new IllegalStateException("Could not unlock keyring");
+       }
+
+       protected Provider getSecurityProvider() {
+               return Security.getProvider(securityProviderName);
+       }
+
+       public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) {
+               this.defaultCallbackHandler = defaultCallbackHandler;
+       }
+
+       public void setCharset(String charset) {
+               this.charset = charset;
+       }
+
+       public void setSecurityProviderName(String securityProviderName) {
+               this.securityProviderName = securityProviderName;
+       }
+
+       // @Deprecated
+       // protected static byte[] hash(char[] password, byte[] salt, Integer
+       // iterationCount) {
+       // ByteArrayOutputStream out = null;
+       // OutputStreamWriter writer = null;
+       // try {
+       // out = new ByteArrayOutputStream();
+       // writer = new OutputStreamWriter(out, "UTF-8");
+       // writer.write(password);
+       // MessageDigest pwDigest = MessageDigest.getInstance("SHA-256");
+       // pwDigest.reset();
+       // pwDigest.update(salt);
+       // byte[] btPass = pwDigest.digest(out.toByteArray());
+       // for (int i = 0; i < iterationCount; i++) {
+       // pwDigest.reset();
+       // btPass = pwDigest.digest(btPass);
+       // }
+       // return btPass;
+       // } catch (Exception e) {
+       // throw new CmsException("Cannot hash", e);
+       // } finally {
+       // IOUtils.closeQuietly(out);
+       // IOUtils.closeQuietly(writer);
+       // }
+       //
+       // }
+
+       /**
+        * Convenience method using the underlying callback to ask for a password
+        * (typically used when the password is not saved in the keyring)
+        */
+       protected char[] ask() {
+               PasswordCallback passwordCb = new PasswordCallback("Password", false);
+               Callback[] dialogCbs = new Callback[] { passwordCb };
+               try {
+                       defaultCallbackHandler.handle(dialogCbs);
+                       char[] password = passwordCb.getPassword();
+                       return password;
+               } catch (Exception e) {
+                       throw new IllegalStateException("Cannot ask for a password", e);
+               }
+
+       }
+
+       class KeyringCallbackHandler implements CallbackHandler {
+               public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+                       // checks
+                       if (callbacks.length != 2)
+                               throw new IllegalArgumentException(
+                                               "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
+                       if (!(callbacks[0] instanceof PasswordCallback))
+                               throw new UnsupportedCallbackException(callbacks[0]);
+                       if (!(callbacks[1] instanceof PBEKeySpecCallback))
+                               throw new UnsupportedCallbackException(callbacks[0]);
+
+                       PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
+                       PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
+
+                       if (isSetup()) {
+                               Callback[] dialogCbs = new Callback[] { passwordCb };
+                               defaultCallbackHandler.handle(dialogCbs);
+                       } else {// setup keyring
+                               TextOutputCallback textCb1 = new TextOutputCallback(TextOutputCallback.INFORMATION,
+                                               "Enter a master password which will protect your private data");
+                               TextOutputCallback textCb2 = new TextOutputCallback(TextOutputCallback.INFORMATION,
+                                               "(for example your credentials to third-party services)");
+                               TextOutputCallback textCb3 = new TextOutputCallback(TextOutputCallback.INFORMATION,
+                                               "Don't forget this password since the data cannot be read without it");
+                               PasswordCallback confirmPasswordCb = new PasswordCallback("Confirm password", false);
+                               // first try
+                               Callback[] dialogCbs = new Callback[] { textCb1, textCb2, textCb3, passwordCb, confirmPasswordCb };
+                               defaultCallbackHandler.handle(dialogCbs);
+
+                               // if passwords different, retry (except if cancelled)
+                               while (passwordCb.getPassword() != null
+                                               && !Arrays.equals(passwordCb.getPassword(), confirmPasswordCb.getPassword())) {
+                                       TextOutputCallback textCb = new TextOutputCallback(TextOutputCallback.ERROR,
+                                                       "The passwords do not match");
+                                       dialogCbs = new Callback[] { textCb, passwordCb, confirmPasswordCb };
+                                       defaultCallbackHandler.handle(dialogCbs);
+                               }
+
+                               if (passwordCb.getPassword() != null) {// not cancelled
+                                       setup(passwordCb.getPassword());
+                               }
+                       }
+
+                       if (passwordCb.getPassword() != null)
+                               handleKeySpecCallback(pbeCb);
+               }
+
+       }
+
+       class PasswordProvidedCallBackHandler implements CallbackHandler {
+               private final char[] password;
+
+               public PasswordProvidedCallBackHandler(char[] password) {
+                       this.password = password;
+               }
+
+               @Override
+               public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+                       // checks
+                       if (callbacks.length != 2)
+                               throw new IllegalArgumentException(
+                                               "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
+                       if (!(callbacks[0] instanceof PasswordCallback))
+                               throw new UnsupportedCallbackException(callbacks[0]);
+                       if (!(callbacks[1] instanceof PBEKeySpecCallback))
+                               throw new UnsupportedCallbackException(callbacks[0]);
+
+                       PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
+                       passwordCb.setPassword(password);
+                       PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
+                       handleKeySpecCallback(pbeCb);
+               }
+
+       }
+}
index 728884b527c33e9eb77147d280884b92ef88b242..3e7d31e3e3e3dc291030a2e06e91743b80eb9c7c 100644 (file)
@@ -7,9 +7,9 @@ import java.util.Set;
 
 import javax.security.auth.Subject;
 
+import org.argeo.api.cms.directory.HierarchyUnit;
 import org.argeo.cms.auth.SystemRole;
-import org.argeo.osgi.useradmin.UserDirectory;
-import org.argeo.util.directory.HierarchyUnit;
+import org.argeo.cms.osgi.useradmin.UserDirectory;
 import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.service.useradmin.Group;
 import org.osgi.service.useradmin.Role;
index ce05dc14c904e41ee5279725f864c346d7d0de94..16f39609e8aafb1c9185dec0a274ef74431cdedf 100644 (file)
@@ -17,7 +17,7 @@ import org.argeo.api.acr.CrName;
 import org.argeo.api.acr.NamespaceUtils;
 import org.argeo.api.acr.spi.ProvidedContent;
 import org.argeo.api.acr.spi.ProvidedSession;
-import org.argeo.util.LangUtils;
+import org.argeo.cms.util.LangUtils;
 
 /** Partial reference implementation of a {@link ProvidedContent}. */
 public abstract class AbstractContent extends AbstractMap<QName, Object> implements ProvidedContent {
index a2d8069dd26e7c1698ca8884efe828c2c73f10a4..98a2fec9304699b6f09b8ac8242ae94dcc3137a9 100644 (file)
@@ -17,6 +17,7 @@ import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamResult;
 
+import org.argeo.api.acr.ArgeoNamespace;
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.CrName;
 import org.argeo.api.acr.NamespaceUtils;
@@ -117,7 +118,7 @@ public abstract class AbstractContentRepository implements ProvidedRepository {
 //                             document = dBuilder.parse(inputSource);
 //                     } else {
                        document = dBuilder.newDocument();
-                       Element root = document.createElementNS(CrName.CR_NAMESPACE_URI,
+                       Element root = document.createElementNS(ArgeoNamespace.CR_NAMESPACE_URI,
                                        NamespaceUtils.toPrefixedName(CrName.root.qName()));
 
                        for (String prefix : RuntimeNamespaceContext.getPrefixes().keySet()) {
index 474e07232d68d2bdae08eb3e456672b4010a2aad..2b4de14da254ba933ab409f01dc9464cf3e7a503 100644 (file)
@@ -17,7 +17,7 @@ import org.argeo.api.cms.DataAdminPrincipal;
 import org.argeo.api.uuid.UuidFactory;
 import org.argeo.cms.auth.CurrentUser;
 import org.argeo.cms.internal.runtime.CmsContextImpl;
-import org.argeo.util.CurrentSubject;
+import org.argeo.cms.util.CurrentSubject;
 
 /**
  * Multi-session {@link ProvidedRepository}, integrated with a CMS.
index fff40c1bb117008fc83551d588b9a9009b0f78e1..9caa4e65746a1c29c7a28ae29691b7df9da59839 100644 (file)
@@ -4,13 +4,13 @@ import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.Objects;
 
-import org.argeo.api.acr.CrName;
+import org.argeo.api.acr.ArgeoNamespace;
 
 public enum CmsContentTypes {
        //
        // ARGEO
        //
-       CR_2(CrName.CR_DEFAULT_PREFIX, CrName.CR_NAMESPACE_URI, "cr.xsd", null),
+       CR_2(ArgeoNamespace.CR_DEFAULT_PREFIX, ArgeoNamespace.CR_NAMESPACE_URI, "cr.xsd", null),
        //
        SLC("slc", "http://www.argeo.org/ns/slc", null, null),
        //
index 74e93fc0affa701c78233641e985d4e14f36a2cf..a6acb8a34bc9505e2c44badbbfe65b94941c2d5e 100644 (file)
@@ -15,11 +15,11 @@ import org.argeo.api.acr.ContentRepository;
 import org.argeo.api.acr.ContentSession;
 import org.argeo.api.acr.DName;
 import org.argeo.api.cms.CmsAuth;
+import org.argeo.api.cms.directory.Directory;
+import org.argeo.api.cms.directory.HierarchyUnit;
 import org.argeo.cms.CmsUserManager;
-import org.argeo.osgi.useradmin.UserDirectory;
-import org.argeo.util.CurrentSubject;
-import org.argeo.util.directory.Directory;
-import org.argeo.util.directory.HierarchyUnit;
+import org.argeo.cms.osgi.useradmin.UserDirectory;
+import org.argeo.cms.util.CurrentSubject;
 import org.osgi.service.useradmin.Role;
 
 /** Utilities and routines around {@link Content}. */
index 5e7c191e269c5f3fef0ff401db2af253fee6149c..8c87c5a938cd2613cc2b690edfd0a7c2ab695db1 100644 (file)
@@ -10,11 +10,11 @@ import javax.security.auth.Subject;
 import javax.security.auth.x500.X500Principal;
 
 import org.argeo.api.acr.ContentSession;
+import org.argeo.api.acr.ldap.LdapAttrs;
 import org.argeo.api.acr.spi.ProvidedRepository;
 import org.argeo.api.uuid.MacAddressUuidFactory;
 import org.argeo.api.uuid.UuidFactory;
 import org.argeo.cms.acr.fs.FsContentProvider;
-import org.argeo.util.naming.LdapAttrs;
 
 /**
  * A standalone {@link ProvidedRepository} with a single {@link Subject} (which
index 1313704f0feb890bad47985067a455028eb7e948..96e6eeaf3836c57887a6acb3c70802d2cf3a1909 100644 (file)
@@ -17,7 +17,7 @@ import org.argeo.api.acr.spi.ProvidedSession;
 import org.argeo.cms.acr.AbstractContent;
 import org.argeo.cms.acr.ContentUtils;
 import org.argeo.cms.dav.DavResponse;
-import org.argeo.util.http.HttpStatus;
+import org.argeo.cms.http.HttpStatus;
 
 public class DavContent extends AbstractContent {
        private final DavContentProvider provider;
index bc4bbfed3136c973c3700e28ad5f54e38c9d5422..374fcebbcdb21bda60c140e21a9b08c639f83864 100644 (file)
@@ -10,7 +10,7 @@ import org.argeo.api.acr.spi.ProvidedContent;
 import org.argeo.api.acr.spi.ProvidedSession;
 import org.argeo.cms.dav.DavClient;
 import org.argeo.cms.dav.DavResponse;
-import org.argeo.util.http.HttpStatus;
+import org.argeo.cms.http.HttpStatus;
 
 public class DavContentProvider implements ContentProvider {
        private String mountPath;
index 2d337c0dda38d78bdfc5d78a428a6ae110942cd8..ac0a7317fba008f187be64fdc14d011a7ccd0b2f 100644 (file)
@@ -10,15 +10,15 @@ import java.util.TreeSet;
 
 import javax.xml.namespace.QName;
 
+import org.argeo.api.acr.ArgeoNamespace;
 import org.argeo.api.acr.ContentName;
 import org.argeo.api.acr.CrAttributeType;
-import org.argeo.api.acr.CrName;
 import org.argeo.api.acr.NamespaceUtils;
+import org.argeo.api.acr.ldap.LdapAttrs;
+import org.argeo.api.acr.ldap.LdapObjs;
 import org.argeo.api.acr.spi.ContentProvider;
 import org.argeo.api.acr.spi.ProvidedSession;
 import org.argeo.cms.acr.AbstractContent;
-import org.argeo.util.naming.LdapAttrs;
-import org.argeo.util.naming.LdapObjs;
 
 abstract class AbstractDirectoryContent extends AbstractContent {
        protected final DirectoryContentProvider provider;
@@ -54,7 +54,7 @@ abstract class AbstractDirectoryContent extends AbstractContent {
                                continue keys;
                        if (key.equalsIgnoreCase(LdapAttrs.objectClasses.name()))
                                continue keys;
-                       ContentName name = new ContentName(CrName.LDAP_NAMESPACE_URI, key, provider);
+                       ContentName name = new ContentName(ArgeoNamespace.LDAP_NAMESPACE_URI, key, provider);
                        keys.add(name);
                }
                return keys;
@@ -65,7 +65,7 @@ abstract class AbstractDirectoryContent extends AbstractContent {
                Dictionary<String, Object> properties = doGetProperties();
                List<QName> contentClasses = new ArrayList<>();
                String objectClass = properties.get(LdapAttrs.objectClass.name()).toString();
-               contentClasses.add(new ContentName(CrName.LDAP_NAMESPACE_URI, objectClass, provider));
+               contentClasses.add(new ContentName(ArgeoNamespace.LDAP_NAMESPACE_URI, objectClass, provider));
 
                String[] objectClasses = properties.get(LdapAttrs.objectClasses.name()).toString().split("\\n");
                objectClasses: for (String oc : objectClasses) {
@@ -73,7 +73,7 @@ abstract class AbstractDirectoryContent extends AbstractContent {
                                continue objectClasses;
                        if (objectClass.equalsIgnoreCase(oc))
                                continue objectClasses;
-                       contentClasses.add(new ContentName(CrName.LDAP_NAMESPACE_URI, oc, provider));
+                       contentClasses.add(new ContentName(ArgeoNamespace.LDAP_NAMESPACE_URI, oc, provider));
                }
                return contentClasses;
        }
index 4e738ae2b8399e315d68a45816373191e34725b9..992f0b41b2eedd87b06baf5ef6fac20c22ce83e2 100644 (file)
@@ -10,8 +10,8 @@ import javax.xml.namespace.QName;
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.ContentName;
 import org.argeo.api.acr.spi.ProvidedSession;
-import org.argeo.util.directory.Directory;
-import org.argeo.util.directory.HierarchyUnit;
+import org.argeo.api.cms.directory.Directory;
+import org.argeo.api.cms.directory.HierarchyUnit;
 
 class DirectoryContent extends AbstractDirectoryContent {
        private Directory directory;
index aab5d6dc06a56ab69cdd533a800450c38f01c8a9..08171435cbb8ade0442f534f9206c0dcbd11edc2 100644 (file)
@@ -7,18 +7,18 @@ import java.util.List;
 
 import javax.xml.namespace.QName;
 
+import org.argeo.api.acr.ArgeoNamespace;
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.ContentName;
 import org.argeo.api.acr.ContentNotFoundException;
-import org.argeo.api.acr.CrName;
 import org.argeo.api.acr.spi.ContentProvider;
 import org.argeo.api.acr.spi.ProvidedContent;
 import org.argeo.api.acr.spi.ProvidedSession;
+import org.argeo.api.cms.directory.HierarchyUnit;
 import org.argeo.cms.CmsUserManager;
 import org.argeo.cms.acr.AbstractContent;
 import org.argeo.cms.acr.ContentUtils;
-import org.argeo.osgi.useradmin.UserDirectory;
-import org.argeo.util.directory.HierarchyUnit;
+import org.argeo.cms.osgi.useradmin.UserDirectory;
 import org.osgi.service.useradmin.User;
 
 public class DirectoryContentProvider implements ContentProvider {
@@ -92,16 +92,16 @@ public class DirectoryContentProvider implements ContentProvider {
 
        @Override
        public String getNamespaceURI(String prefix) {
-               if (CrName.LDAP_DEFAULT_PREFIX.equals(prefix))
-                       return CrName.LDAP_NAMESPACE_URI;
-               throw new IllegalArgumentException("Only prefix " + CrName.LDAP_DEFAULT_PREFIX + " is supported");
+               if (ArgeoNamespace.LDAP_DEFAULT_PREFIX.equals(prefix))
+                       return ArgeoNamespace.LDAP_NAMESPACE_URI;
+               throw new IllegalArgumentException("Only prefix " + ArgeoNamespace.LDAP_DEFAULT_PREFIX + " is supported");
        }
 
        @Override
        public Iterator<String> getPrefixes(String namespaceURI) {
-               if (CrName.LDAP_NAMESPACE_URI.equals(namespaceURI))
-                       return Collections.singletonList(CrName.LDAP_DEFAULT_PREFIX).iterator();
-               throw new IllegalArgumentException("Only namespace URI " + CrName.LDAP_NAMESPACE_URI + " is supported");
+               if (ArgeoNamespace.LDAP_NAMESPACE_URI.equals(namespaceURI))
+                       return Collections.singletonList(ArgeoNamespace.LDAP_DEFAULT_PREFIX).iterator();
+               throw new IllegalArgumentException("Only namespace URI " + ArgeoNamespace.LDAP_NAMESPACE_URI + " is supported");
        }
 
        public void setUserManager(CmsUserManager userManager) {
index 1e4aad77328a7805df4732e51935eafc71e5a047..feae4b5176dcbe318b1e28def01928c81a5b026e 100644 (file)
@@ -13,9 +13,9 @@ import org.argeo.api.acr.ContentName;
 import org.argeo.api.acr.CrName;
 import org.argeo.api.acr.DName;
 import org.argeo.api.acr.spi.ProvidedSession;
-import org.argeo.osgi.useradmin.UserDirectory;
-import org.argeo.util.directory.Directory;
-import org.argeo.util.directory.HierarchyUnit;
+import org.argeo.api.cms.directory.Directory;
+import org.argeo.api.cms.directory.HierarchyUnit;
+import org.argeo.cms.osgi.useradmin.UserDirectory;
 import org.osgi.service.useradmin.Role;
 
 class HierarchyUnitContent extends AbstractDirectoryContent {
index 7aa144633689c5d668f1297aa4ad7be81db434ac..3b1ae46b29b5dfd31fe3cbc89f00c72bcffa2b0d 100644 (file)
@@ -7,7 +7,7 @@ import javax.xml.namespace.QName;
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.ContentName;
 import org.argeo.api.acr.spi.ProvidedSession;
-import org.argeo.osgi.useradmin.UserDirectory;
+import org.argeo.cms.osgi.useradmin.UserDirectory;
 import org.osgi.service.useradmin.Group;
 import org.osgi.service.useradmin.Role;
 import org.osgi.service.useradmin.User;
index 2d30cedec3b8f936ae395e175fd8ec539d4cd7c2..43cae85721bf6b96c3f577f157f64b56845c11b2 100644 (file)
@@ -44,7 +44,7 @@ import org.argeo.api.acr.spi.ProvidedSession;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.cms.acr.AbstractContent;
 import org.argeo.cms.acr.ContentUtils;
-import org.argeo.util.FsUtils;
+import org.argeo.cms.util.FsUtils;
 
 /** Content persisted as a filesystem {@link Path}. */
 public class FsContent extends AbstractContent implements ProvidedContent {
index 59a4d8deb5616f12c6b1b24c61406726f3f121b2..9b1b9668303f066e2106de3aa4a296edcabbf55d 100644 (file)
@@ -13,8 +13,8 @@ import java.util.Objects;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
 
+import org.argeo.api.acr.ArgeoNamespace;
 import org.argeo.api.acr.ContentResourceException;
-import org.argeo.api.acr.CrName;
 import org.argeo.api.acr.NamespaceUtils;
 import org.argeo.api.acr.RuntimeNamespaceContext;
 import org.argeo.api.acr.spi.ContentProvider;
@@ -62,10 +62,10 @@ public class FsContentProvider implements ContentProvider {
                        }
 
                        // defaults
-                       addDefaultNamespace(udfav, CrName.CR_DEFAULT_PREFIX, CrName.CR_NAMESPACE_URI);
-                       addDefaultNamespace(udfav, "basic", CrName.CR_NAMESPACE_URI);
-                       addDefaultNamespace(udfav, "owner", CrName.CR_NAMESPACE_URI);
-                       addDefaultNamespace(udfav, "posix", CrName.CR_NAMESPACE_URI);
+                       addDefaultNamespace(udfav, ArgeoNamespace.CR_DEFAULT_PREFIX, ArgeoNamespace.CR_NAMESPACE_URI);
+                       addDefaultNamespace(udfav, "basic", ArgeoNamespace.CR_NAMESPACE_URI);
+                       addDefaultNamespace(udfav, "owner", ArgeoNamespace.CR_NAMESPACE_URI);
+                       addDefaultNamespace(udfav, "posix", ArgeoNamespace.CR_NAMESPACE_URI);
                } catch (IOException e) {
                        throw new RuntimeException("Cannot read namespaces from " + rootPath, e);
                }
index ea1a17d7e88efd5ec67d377050db30b0909b7597..3f747622195943cb617d1391b344c7b69275eedb 100644 (file)
@@ -3,6 +3,7 @@ package org.argeo.cms.acr.xml;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
 
+import org.argeo.api.acr.ArgeoNamespace;
 import org.argeo.api.acr.Content;
 import org.argeo.api.acr.CrName;
 import org.argeo.api.acr.spi.ProvidedSession;
@@ -52,7 +53,7 @@ class ElementIterator implements Iterator<Content> {
                if (nextElement == null)
                        throw new NoSuchElementException();
                Content result;
-               String isMount = nextElement.getAttributeNS(CrName.CR_NAMESPACE_URI, CrName.mount.qName().getLocalPart());
+               String isMount = nextElement.getAttributeNS(ArgeoNamespace.CR_NAMESPACE_URI, CrName.mount.qName().getLocalPart());
                if (isMount.equals("true")) {
                        result = session.get(parent.getPath() + '/' + nextElement.getTagName());
                }
index d7d6c282ca7fb24ee4a58e2eddcc9aa513146912..289f8dcc65eabf2e101c0af6208bbb2ead05ff90 100644 (file)
@@ -27,7 +27,7 @@ import org.argeo.cms.internal.auth.CmsSessionImpl;
 import org.argeo.cms.internal.auth.ImpliedByPrincipal;
 import org.argeo.cms.internal.auth.RemoteCmsSessionImpl;
 import org.argeo.cms.internal.runtime.CmsContextImpl;
-import org.argeo.osgi.useradmin.AuthenticatingUser;
+import org.argeo.cms.osgi.useradmin.AuthenticatingUser;
 import org.osgi.service.useradmin.Authorization;
 
 /** Centralises security related registrations. */
index 6aa1c1c2561b9b12795f39b91a5e45b54fda519b..5ac23ea6a158e3a140d7b9023de43d6dcf558079 100644 (file)
@@ -2,8 +2,8 @@ package org.argeo.cms.auth;
 
 import javax.xml.namespace.QName;
 
+import org.argeo.api.acr.ArgeoNamespace;
 import org.argeo.api.acr.ContentName;
-import org.argeo.api.acr.CrName;
 
 /** Standard CMS system roles. */
 public enum CmsRole implements SystemRole {
@@ -15,7 +15,7 @@ public enum CmsRole implements SystemRole {
        private final ContentName name;
 
        CmsRole() {
-               name = new ContentName(CrName.ROLE_NAMESPACE_URI, QUALIFIER + name());
+               name = new ContentName(ArgeoNamespace.ROLE_NAMESPACE_URI, QUALIFIER + name());
        }
 
        @Override
index 2fd8730d8aafd8c77fbf2aae0f750025e3f1d5bf..ee522a580d8417c4d1f0aac52cd240024291ed11 100644 (file)
@@ -20,7 +20,7 @@ import org.argeo.api.cms.CmsSessionId;
 import org.argeo.cms.internal.auth.CmsSessionImpl;
 import org.argeo.cms.internal.auth.ImpliedByPrincipal;
 import org.argeo.cms.internal.runtime.CmsContextImpl;
-import org.argeo.util.CurrentSubject;
+import org.argeo.cms.util.CurrentSubject;
 import org.osgi.service.useradmin.Authorization;
 
 /**
index af559df7558026fc187dbf055181b86dd0deaa68..ebab12f2cc4cd24149787663aa52dad44c651961 100644 (file)
@@ -14,9 +14,9 @@ import javax.security.auth.callback.PasswordCallback;
 import javax.security.auth.login.LoginException;
 import javax.security.auth.spi.LoginModule;
 
-import org.argeo.cms.security.PBEKeySpecCallback;
-import org.argeo.util.CurrentSubject;
-import org.argeo.util.PasswordEncryption;
+import org.argeo.api.cms.keyring.PBEKeySpecCallback;
+import org.argeo.cms.util.CurrentSubject;
+import org.argeo.cms.util.PasswordEncryption;
 
 /** Adds a secret key to the private credentials */
 public class KeyringLoginModule implements LoginModule {
index 785eeb912f727a5a1246d41927536167f5830a26..4a8f18fcd811a11b516597e827d173542a441c50 100644 (file)
@@ -12,10 +12,10 @@ import javax.security.auth.login.LoginException;
 import org.argeo.api.cms.CmsAuth;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.api.cms.CmsSession;
+import org.argeo.cms.http.HttpHeader;
+import org.argeo.cms.http.HttpStatus;
 import org.argeo.cms.internal.runtime.CmsContextImpl;
-import org.argeo.util.CurrentSubject;
-import org.argeo.util.http.HttpHeader;
-import org.argeo.util.http.HttpStatus;
+import org.argeo.cms.util.CurrentSubject;
 import org.ietf.jgss.GSSContext;
 import org.ietf.jgss.GSSException;
 import org.ietf.jgss.GSSManager;
index 772531ede5f53f213a10c8cbc510d275542809f4..987c3dd19dfeff760f579d27adf6f41558812a29 100644 (file)
@@ -16,10 +16,10 @@ import javax.security.auth.spi.LoginModule;
 
 import org.argeo.api.cms.CmsLog;
 import org.argeo.cms.CmsDeployProperty;
+import org.argeo.cms.http.HttpHeader;
 import org.argeo.cms.internal.auth.CmsSessionImpl;
 import org.argeo.cms.internal.runtime.CmsContextImpl;
 import org.argeo.cms.internal.runtime.CmsStateImpl;
-import org.argeo.util.http.HttpHeader;
 import org.osgi.service.useradmin.Authorization;
 
 /** Use a remote session as the basis for authentication. */
index 35b7d5cc7ebe73233f630e1a46bb6aa2bac2168e..23fdb6deecb79e54ece0163faea946b52f1838d4 100644 (file)
@@ -11,10 +11,10 @@ import javax.security.auth.login.LoginException;
 import javax.security.auth.spi.LoginModule;
 import javax.security.auth.x500.X500Principal;
 
+import org.argeo.api.acr.ldap.LdapAttrs;
+import org.argeo.cms.directory.ldap.IpaUtils;
 import org.argeo.cms.internal.runtime.CmsContextImpl;
-import org.argeo.osgi.useradmin.OsUserUtils;
-import org.argeo.util.directory.ldap.IpaUtils;
-import org.argeo.util.naming.LdapAttrs;
+import org.argeo.cms.osgi.useradmin.OsUserUtils;
 import org.osgi.service.useradmin.Authorization;
 
 /** Login module for when the system is owned by a single user. */
index 0ae84ff8ac7f294b4b891ac5d77c7281eb10e162..278321c24480de4c08db1debab26301701672d14 100644 (file)
@@ -1,6 +1,6 @@
 package org.argeo.cms.auth;
 
-import static org.argeo.util.naming.LdapAttrs.cn;
+import static org.argeo.api.acr.ldap.LdapAttrs.cn;
 
 import java.io.IOException;
 import java.security.PrivilegedAction;
@@ -24,13 +24,13 @@ import javax.security.auth.login.CredentialNotFoundException;
 import javax.security.auth.login.LoginException;
 import javax.security.auth.spi.LoginModule;
 
+import org.argeo.api.acr.ldap.LdapAttrs;
 import org.argeo.api.cms.CmsConstants;
 import org.argeo.api.cms.CmsLog;
+import org.argeo.cms.directory.ldap.IpaUtils;
 import org.argeo.cms.internal.runtime.CmsContextImpl;
-import org.argeo.osgi.useradmin.AuthenticatingUser;
-import org.argeo.osgi.useradmin.TokenUtils;
-import org.argeo.util.directory.ldap.IpaUtils;
-import org.argeo.util.naming.LdapAttrs;
+import org.argeo.cms.osgi.useradmin.AuthenticatingUser;
+import org.argeo.cms.osgi.useradmin.TokenUtils;
 import org.osgi.service.useradmin.Authorization;
 import org.osgi.service.useradmin.Group;
 import org.osgi.service.useradmin.User;
index e3eb442492055e7cbcc02b641ef8f44b2301355c..47d2eeb4117cdda2f81974a998dd05cb93839169 100644 (file)
@@ -6,8 +6,8 @@ import javax.naming.InvalidNameException;
 import javax.naming.ldap.LdapName;
 import javax.naming.ldap.Rdn;
 
+import org.argeo.api.acr.ldap.LdapAttrs;
 import org.argeo.api.cms.CmsConstants;
-import org.argeo.util.naming.LdapAttrs;
 import org.osgi.service.useradmin.Role;
 import org.osgi.service.useradmin.User;
 import org.osgi.service.useradmin.UserAdmin;
index af0656097702cbc6e9d24ff46f9b2469fac38759..8cfae3a1b8cd09fa7162e80758238b0c1c19397e 100644 (file)
@@ -30,7 +30,7 @@ import javax.security.auth.login.LoginException;
 
 import org.argeo.cms.auth.ConsoleCallbackHandler;
 import org.argeo.cms.auth.RemoteAuthUtils;
-import org.argeo.util.http.HttpHeader;
+import org.argeo.cms.http.HttpHeader;
 
 /** Utility to connect to a remote CMS node. */
 public class CmsClient {
index 94f292c8e68cd70e2b3c38201a04959332cf0904..6fe2eb617cf0862c8ce62b121d4a3edd481153c1 100644 (file)
@@ -15,9 +15,9 @@ import java.util.Iterator;
 
 import javax.xml.namespace.QName;
 
-import org.argeo.util.http.HttpHeader;
-import org.argeo.util.http.HttpMethod;
-import org.argeo.util.http.HttpStatus;
+import org.argeo.cms.http.HttpHeader;
+import org.argeo.cms.http.HttpMethod;
+import org.argeo.cms.http.HttpStatus;
 
 public class DavClient {
 
index 24695e7b1fc52c737503bc134898a910dfe18b53..c7542b55aa7f551c7c7338c8fd79ae5f71711ebf 100644 (file)
@@ -1,6 +1,6 @@
 package org.argeo.cms.dav;
 
-import org.argeo.util.http.HttpHeader;
+import org.argeo.cms.http.HttpHeader;
 
 import com.sun.net.httpserver.HttpExchange;
 
index 1d6c02623d7bf40d3049b53849b704e437547ef5..9fd03f25abb5cfd735cc712ea8938864df4d0b93 100644 (file)
@@ -10,10 +10,10 @@ import java.util.function.Consumer;
 import javax.xml.namespace.NamespaceContext;
 
 import org.argeo.api.acr.ContentNotFoundException;
-import org.argeo.util.http.HttpHeader;
-import org.argeo.util.http.HttpMethod;
-import org.argeo.util.http.HttpStatus;
-import org.argeo.util.http.HttpServerUtils;
+import org.argeo.cms.http.HttpHeader;
+import org.argeo.cms.http.HttpMethod;
+import org.argeo.cms.http.HttpServerUtils;
+import org.argeo.cms.http.HttpStatus;
 
 import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpHandler;
index 828dc2640902d7571e91fa0b1cc8e6aa3b2f76df..8dd6bf3fc2cde0742d39675f550f74a271791656 100644 (file)
@@ -10,7 +10,7 @@ import java.util.TreeSet;
 
 import javax.xml.namespace.QName;
 
-import org.argeo.util.http.HttpStatus;
+import org.argeo.cms.http.HttpStatus;
 
 /** The WebDav response for a given resource. */
 public class DavResponse {
index 6d22c8e295f959822cf3ffb0a0ebfa6868d1c22e..c7b54b0273324aa3cf273025fc9db09d0be03bed 100644 (file)
@@ -19,7 +19,7 @@ import javax.xml.stream.XMLStreamConstants;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamReader;
 
-import org.argeo.util.http.HttpStatus;
+import org.argeo.cms.http.HttpStatus;
 
 /**
  * Asynchronously iterate over the response statuses of the response to a
index 986b2fe9271c665fda435b399f7124d37e9a0a14..4689b8c8d346c67262512b5c2025b4b80eebfe54 100644 (file)
@@ -20,7 +20,7 @@ import javax.xml.stream.XMLOutputFactory;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamWriter;
 
-import org.argeo.util.http.HttpStatus;
+import org.argeo.cms.http.HttpStatus;
 
 class MultiStatusWriter implements Consumer<DavResponse> {
        private BlockingQueue<DavResponse> queue = new ArrayBlockingQueue<>(64);
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectory.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectory.java
new file mode 100644 (file)
index 0000000..06c33b0
--- /dev/null
@@ -0,0 +1,572 @@
+package org.argeo.cms.directory.ldap;
+
+import static org.argeo.cms.directory.ldap.LdapNameUtils.toLdapName;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.StringJoiner;
+
+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.BasicAttributes;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import javax.transaction.xa.XAResource;
+
+import org.argeo.api.acr.ldap.LdapAttrs;
+import org.argeo.api.acr.ldap.LdapObjs;
+import org.argeo.api.cms.directory.Directory;
+import org.argeo.api.cms.directory.HierarchyUnit;
+import org.argeo.api.cms.transaction.WorkControl;
+import org.argeo.api.cms.transaction.WorkingCopyXaResource;
+import org.argeo.api.cms.transaction.XAResourceProvider;
+import org.argeo.cms.osgi.useradmin.OsUserDirectory;
+import org.argeo.cms.runtime.DirectoryConf;
+
+/** A {@link Directory} based either on LDAP or LDIF. */
+public abstract class AbstractLdapDirectory implements Directory, XAResourceProvider {
+       protected static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name";
+       protected static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password";
+
+       private final LdapName baseDn;
+       private final Hashtable<String, Object> configProperties;
+       private final Rdn userBaseRdn, groupBaseRdn, systemRoleBaseRdn;
+       private final String userObjectClass, groupObjectClass;
+       private String memberAttributeId = "member";
+
+       private final boolean readOnly;
+       private final boolean disabled;
+       private final String uri;
+
+       private String forcedPassword;
+
+       private final boolean scoped;
+
+       private List<String> credentialAttributeIds = Arrays
+                       .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
+
+       private WorkControl transactionControl;
+       private WorkingCopyXaResource<LdapEntryWorkingCopy> xaResource;
+
+       private LdapDirectoryDao directoryDao;
+
+       /** Whether the the directory has is authenticated via a service user. */
+       private boolean authenticated = false;
+
+       public AbstractLdapDirectory(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
+               this.configProperties = new Hashtable<String, Object>();
+               for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
+                       String key = keys.nextElement();
+                       configProperties.put(key, props.get(key));
+               }
+
+               String baseDnStr = DirectoryConf.baseDn.getValue(configProperties);
+               if (baseDnStr == null)
+                       throw new IllegalArgumentException("Base DN must be specified: " + configProperties);
+               baseDn = toLdapName(baseDnStr);
+               this.scoped = scoped;
+
+               if (uriArg != null) {
+                       uri = uriArg.toString();
+                       // uri from properties is ignored
+               } else {
+                       String uriStr = DirectoryConf.uri.getValue(configProperties);
+                       if (uriStr == null)
+                               uri = null;
+                       else
+                               uri = uriStr;
+               }
+
+               forcedPassword = DirectoryConf.forcedPassword.getValue(configProperties);
+
+               userObjectClass = DirectoryConf.userObjectClass.getValue(configProperties);
+               groupObjectClass = DirectoryConf.groupObjectClass.getValue(configProperties);
+
+               String userBase = DirectoryConf.userBase.getValue(configProperties);
+               String groupBase = DirectoryConf.groupBase.getValue(configProperties);
+               String systemRoleBase = DirectoryConf.systemRoleBase.getValue(configProperties);
+               try {
+//                     baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
+                       userBaseRdn = new Rdn(userBase);
+//                     userBaseDn = new LdapName(userBase + "," + baseDn);
+                       groupBaseRdn = new Rdn(groupBase);
+//                     groupBaseDn = new LdapName(groupBase + "," + baseDn);
+                       systemRoleBaseRdn = new Rdn(systemRoleBase);
+               } catch (InvalidNameException e) {
+                       throw new IllegalArgumentException(
+                                       "Badly formated base DN " + DirectoryConf.baseDn.getValue(configProperties), e);
+               }
+
+               // read only
+               String readOnlyStr = DirectoryConf.readOnly.getValue(configProperties);
+               if (readOnlyStr == null) {
+                       readOnly = readOnlyDefault(uri);
+                       configProperties.put(DirectoryConf.readOnly.name(), Boolean.toString(readOnly));
+               } else
+                       readOnly = Boolean.parseBoolean(readOnlyStr);
+
+               // disabled
+               String disabledStr = DirectoryConf.disabled.getValue(configProperties);
+               if (disabledStr != null)
+                       disabled = Boolean.parseBoolean(disabledStr);
+               else
+                       disabled = false;
+               if (!getRealm().isEmpty()) {
+                       // IPA multiple LDAP causes URI parsing to fail
+                       // TODO manage generic redundant LDAP case
+                       directoryDao = new LdapDao(this);
+               } else {
+                       if (uri != null) {
+                               URI u = URI.create(uri);
+                               if (DirectoryConf.SCHEME_LDAP.equals(u.getScheme())
+                                               || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) {
+                                       directoryDao = new LdapDao(this);
+                                       authenticated = configProperties.get(Context.SECURITY_PRINCIPAL) != null;
+                               } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) {
+                                       directoryDao = new LdifDao(this);
+                                       authenticated = true;
+                               } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) {
+                                       directoryDao = new OsUserDirectory(this);
+                                       authenticated = true;
+                                       // singleUser = true;
+                               } else {
+                                       throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
+                               }
+                       } else {
+                               // in memory
+                               directoryDao = new LdifDao(this);
+                       }
+               }
+               if (directoryDao != null)
+                       xaResource = new WorkingCopyXaResource<>(directoryDao);
+       }
+
+       /*
+        * INITIALISATION
+        */
+
+       public void init() {
+               getDirectoryDao().init();
+       }
+
+       public void destroy() {
+               getDirectoryDao().destroy();
+       }
+
+       /*
+        * CREATION
+        */
+       protected abstract LdapEntry newUser(LdapName name);
+
+       protected abstract LdapEntry newGroup(LdapName name);
+
+       /*
+        * EDITION
+        */
+
+       public boolean isEditing() {
+               return xaResource.wc() != null;
+       }
+
+       public LdapEntryWorkingCopy getWorkingCopy() {
+               LdapEntryWorkingCopy wc = xaResource.wc();
+               if (wc == null)
+                       return null;
+               return wc;
+       }
+
+       public void checkEdit() {
+               if (xaResource.wc() == null) {
+                       try {
+                               transactionControl.getWorkContext().registerXAResource(xaResource, null);
+                       } catch (Exception e) {
+                               throw new IllegalStateException("Cannot enlist " + xaResource, e);
+                       }
+               } else {
+               }
+       }
+
+       public void setTransactionControl(WorkControl transactionControl) {
+               this.transactionControl = transactionControl;
+       }
+
+       public XAResource getXaResource() {
+               return xaResource;
+       }
+
+       public boolean removeEntry(LdapName dn) {
+               checkEdit();
+               LdapEntryWorkingCopy wc = getWorkingCopy();
+               boolean actuallyDeleted;
+               if (getDirectoryDao().entryExists(dn) || wc.getNewData().containsKey(dn)) {
+                       LdapEntry user = doGetRole(dn);
+                       wc.getDeletedData().put(dn, user);
+                       actuallyDeleted = true;
+               } else {// just removing from groups (e.g. system roles)
+                       actuallyDeleted = false;
+               }
+               for (LdapName groupDn : getDirectoryDao().getDirectGroups(dn)) {
+                       LdapEntry group = doGetRole(groupDn);
+                       group.getAttributes().get(getMemberAttributeId()).remove(dn.toString());
+               }
+               return actuallyDeleted;
+       }
+
+       /*
+        * RETRIEVAL
+        */
+
+       protected LdapEntry doGetRole(LdapName dn) {
+               LdapEntryWorkingCopy wc = getWorkingCopy();
+               LdapEntry user;
+               try {
+                       user = getDirectoryDao().doGetEntry(dn);
+               } catch (NameNotFoundException e) {
+                       user = null;
+               }
+               if (wc != null) {
+                       if (user == null && wc.getNewData().containsKey(dn))
+                               user = wc.getNewData().get(dn);
+                       else if (wc.getDeletedData().containsKey(dn))
+                               user = null;
+               }
+               return user;
+       }
+
+       protected void collectGroups(LdapEntry user, List<LdapEntry> 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 memberOf
+               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());
+                                       LdapEntry group = doGetRole(groupDn);
+                                       if (group != null) {
+                                               allRoles.add(group);
+                                       } else {
+                                               // user doesn't have the right to retrieve role, but we know it exists
+                                               // otherwise memberOf would not work
+                                               group = newGroup(groupDn);
+                                               allRoles.add(group);
+                                       }
+                               }
+                       } catch (NamingException e) {
+                               throw new IllegalStateException("Cannot get memberOf groups for " + user, e);
+                       }
+               } else {
+                       directGroups: for (LdapName groupDn : getDirectoryDao().getDirectGroups(user.getDn())) {
+                               LdapEntry group = doGetRole(groupDn);
+                               if (group != null) {
+                                       if (allRoles.contains(group)) {
+                                               // important in order to avoi loops
+                                               continue directGroups;
+                                       }
+                                       allRoles.add(group);
+                                       collectGroups(group, allRoles);
+                               }
+                       }
+               }
+       }
+
+       /*
+        * HIERARCHY
+        */
+       @Override
+       public HierarchyUnit getHierarchyUnit(String path) {
+               LdapName dn = pathToName(path);
+               return directoryDao.doGetHierarchyUnit(dn);
+       }
+
+       @Override
+       public Iterable<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly) {
+               return directoryDao.doGetDirectHierarchyUnits(baseDn, functionalOnly);
+       }
+
+       @Override
+       public String getHierarchyUnitName() {
+               return getName();
+       }
+
+       @Override
+       public String getHierarchyUnitLabel(Locale locale) {
+               String key = LdapNameUtils.getLastRdn(getBaseDn()).getType();
+               Object value = LdapEntry.getLocalized(asLdapEntry().getProperties(), key, locale);
+               if (value == null)
+                       value = getHierarchyUnitName();
+               assert value != null;
+               return value.toString();
+       }
+
+       @Override
+       public HierarchyUnit getParent() {
+               return null;
+       }
+
+       @Override
+       public boolean isFunctional() {
+               return true;
+       }
+
+       @Override
+       public Directory getDirectory() {
+               return this;
+       }
+
+       @Override
+       public HierarchyUnit createHierarchyUnit(String path) {
+               checkEdit();
+               LdapEntryWorkingCopy wc = getWorkingCopy();
+               LdapName dn = pathToName(path);
+               if ((getDirectoryDao().entryExists(dn) && !wc.getDeletedData().containsKey(dn))
+                               || wc.getNewData().containsKey(dn))
+                       throw new IllegalArgumentException("Already a hierarchy unit " + path);
+               BasicAttributes attrs = new BasicAttributes(true);
+               attrs.put(LdapAttrs.objectClass.name(), LdapObjs.organizationalUnit.name());
+               Rdn nameRdn = dn.getRdn(dn.size() - 1);
+               // TODO deal with multiple attr RDN
+               attrs.put(nameRdn.getType(), nameRdn.getValue());
+               wc.getModifiedData().put(dn, attrs);
+               LdapHierarchyUnit newHierarchyUnit = new LdapHierarchyUnit(this, dn);
+               wc.getNewData().put(dn, newHierarchyUnit);
+               return newHierarchyUnit;
+       }
+
+       /*
+        * PATHS
+        */
+
+       @Override
+       public String getBase() {
+               return getBaseDn().toString();
+       }
+
+       @Override
+       public String getName() {
+               return nameToSimple(getBaseDn(), ".");
+       }
+
+       protected String nameToRelativePath(LdapName dn) {
+               LdapName name = LdapNameUtils.relativeName(getBaseDn(), dn);
+               return nameToSimple(name, "/");
+       }
+
+       protected String nameToSimple(LdapName name, String separator) {
+               StringJoiner path = new StringJoiner(separator);
+               for (int i = 0; i < name.size(); i++) {
+                       path.add(name.getRdn(i).getValue().toString());
+               }
+               return path.toString();
+
+       }
+
+       protected LdapName pathToName(String path) {
+               try {
+                       LdapName name = (LdapName) getBaseDn().clone();
+                       String[] segments = path.split("/");
+                       Rdn parentRdn = null;
+                       // segments[0] is the directory itself
+                       for (int i = 0; i < segments.length; i++) {
+                               String segment = segments[i];
+                               // TODO make attr names configurable ?
+                               String attr = getDirectory().getRealm().isPresent()/* IPA */ ? LdapAttrs.cn.name()
+                                               : LdapAttrs.ou.name();
+                               if (parentRdn != null) {
+                                       if (getUserBaseRdn().equals(parentRdn))
+                                               attr = LdapAttrs.uid.name();
+                                       else if (getGroupBaseRdn().equals(parentRdn))
+                                               attr = LdapAttrs.cn.name();
+                                       else if (getSystemRoleBaseRdn().equals(parentRdn))
+                                               attr = LdapAttrs.cn.name();
+                               }
+                               Rdn rdn = new Rdn(attr, segment);
+                               name.add(rdn);
+                               parentRdn = rdn;
+                       }
+                       return name;
+               } catch (InvalidNameException e) {
+                       throw new IllegalStateException("Cannot convert " + path + " to LDAP name", e);
+               }
+
+       }
+
+       /*
+        * UTILITIES
+        */
+       protected boolean isExternal(LdapName name) {
+               return !name.startsWith(baseDn);
+       }
+
+       protected static boolean hasObjectClass(Attributes attrs, LdapObjs objectClass) {
+               return hasObjectClass(attrs, objectClass.name());
+       }
+
+       protected static boolean hasObjectClass(Attributes attrs, String objectClass) {
+               try {
+                       Attribute attr = attrs.get(LdapAttrs.objectClass.name());
+                       NamingEnumeration<?> en = attr.getAll();
+                       while (en.hasMore()) {
+                               String v = en.next().toString();
+                               if (v.equalsIgnoreCase(objectClass))
+                                       return true;
+
+                       }
+                       return false;
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot search for objectClass " + objectClass, e);
+               }
+       }
+
+       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(DirectoryConf.SCHEME_FILE)) {
+                       File file = new File(uri);
+                       if (file.exists())
+                               return !file.canWrite();
+                       else
+                               return !file.getParentFile().canWrite();
+               } else if (uri.getScheme().equals(DirectoryConf.SCHEME_LDAP)) {
+                       if (uri.getAuthority() != null)// assume writable if authenticated
+                               return false;
+               } else if (uri.getScheme().equals(DirectoryConf.SCHEME_OS)) {
+                       return true;
+               }
+               return true;// read only by default
+       }
+
+       /*
+        * AS AN ENTRY
+        */
+       public LdapEntry asLdapEntry() {
+               try {
+                       return directoryDao.doGetEntry(baseDn);
+               } catch (NameNotFoundException e) {
+                       throw new IllegalStateException("Cannot get " + baseDn + " entry", e);
+               }
+       }
+
+       public Dictionary<String, Object> getProperties() {
+               return asLdapEntry().getProperties();
+       }
+
+       /*
+        * ACCESSORS
+        */
+       @Override
+       public Optional<String> getRealm() {
+               Object realm = configProperties.get(DirectoryConf.realm.name());
+               if (realm == null)
+                       return Optional.empty();
+               return Optional.of(realm.toString());
+       }
+
+       public LdapName getBaseDn() {
+               return (LdapName) baseDn.clone();
+       }
+
+       public boolean isReadOnly() {
+               return readOnly;
+       }
+
+       public boolean isDisabled() {
+               return disabled;
+       }
+
+       public boolean isAuthenticated() {
+               return authenticated;
+       }
+
+       public Rdn getUserBaseRdn() {
+               return userBaseRdn;
+       }
+
+       public Rdn getGroupBaseRdn() {
+               return groupBaseRdn;
+       }
+
+       public Rdn getSystemRoleBaseRdn() {
+               return systemRoleBaseRdn;
+       }
+
+//     public Dictionary<String, Object> getConfigProperties() {
+//             return configProperties;
+//     }
+
+       public Dictionary<String, Object> cloneConfigProperties() {
+               return new Hashtable<>(configProperties);
+       }
+
+       public String getForcedPassword() {
+               return forcedPassword;
+       }
+
+       public boolean isScoped() {
+               return scoped;
+       }
+
+       public List<String> getCredentialAttributeIds() {
+               return credentialAttributeIds;
+       }
+
+       public String getUri() {
+               return uri;
+       }
+
+       public LdapDirectoryDao getDirectoryDao() {
+               return directoryDao;
+       }
+
+       /** dn can be null, in that case a default should be returned. */
+       public String getUserObjectClass() {
+               return userObjectClass;
+       }
+
+       public String getGroupObjectClass() {
+               return groupObjectClass;
+       }
+
+       public String getMemberAttributeId() {
+               return memberAttributeId;
+       }
+
+       /*
+        * OBJECT METHODS
+        */
+
+       @Override
+       public int hashCode() {
+               return baseDn.hashCode();
+       }
+
+       @Override
+       public String toString() {
+               return "Directory " + baseDn.toString();
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectoryDao.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AbstractLdapDirectoryDao.java
new file mode 100644 (file)
index 0000000..c4a6910
--- /dev/null
@@ -0,0 +1,33 @@
+package org.argeo.cms.directory.ldap;
+
+import javax.naming.ldap.LdapName;
+
+/** Base class for LDAP/LDIF directory DAOs. */
+public abstract class AbstractLdapDirectoryDao implements LdapDirectoryDao {
+
+       private AbstractLdapDirectory directory;
+
+       public AbstractLdapDirectoryDao(AbstractLdapDirectory directory) {
+               this.directory = directory;
+       }
+
+       public AbstractLdapDirectory getDirectory() {
+               return directory;
+       }
+
+       @Override
+       public LdapEntryWorkingCopy newWorkingCopy() {
+               return new LdapEntryWorkingCopy();
+       }
+
+       @Override
+       public LdapEntry newUser(LdapName name) {
+               return getDirectory().newUser(name);
+       }
+
+       @Override
+       public LdapEntry newGroup(LdapName name) {
+               return getDirectory().newGroup(name);
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/AttributesDictionary.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AttributesDictionary.java
new file mode 100644 (file)
index 0000000..9deda1b
--- /dev/null
@@ -0,0 +1,171 @@
+package org.argeo.cms.directory.ldap;
+
+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));
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/AuthPassword.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/AuthPassword.java
new file mode 100644 (file)
index 0000000..f2332db
--- /dev/null
@@ -0,0 +1,140 @@
+package org.argeo.cms.directory.ldap;
+
+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.api.acr.ldap.LdapAttrs;
+
+/** 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 IllegalStateException("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());
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/DefaultLdapEntry.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/DefaultLdapEntry.java
new file mode 100644 (file)
index 0000000..ad70d67
--- /dev/null
@@ -0,0 +1,481 @@
+package org.argeo.cms.directory.ldap;
+
+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.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+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.api.acr.ldap.LdapAttrs;
+import org.argeo.api.acr.ldap.LdapObjs;
+import org.argeo.api.cms.directory.DirectoryDigestUtils;
+
+/** An entry in an LDAP (or LDIF) directory. */
+public class DefaultLdapEntry implements LdapEntry {
+       private final AbstractLdapDirectory directory;
+
+       private final LdapName dn;
+
+       private AttributeDictionary properties;
+       private AttributeDictionary credentials;
+
+//     private String primaryObjectClass;
+//     private List<String> objectClasses = new ArrayList<>();
+
+       protected DefaultLdapEntry(AbstractLdapDirectory directory, LdapName dn) {
+               Objects.requireNonNull(directory);
+               Objects.requireNonNull(dn);
+               this.directory = directory;
+               this.dn = dn;
+
+               // Object classes
+//             Objects.requireNonNull(initialAttributes);
+//             try {
+//                     NamingEnumeration<?> en = initialAttributes.get(LdapAttrs.objectClass.name()).getAll();
+//                     String first = null;
+//                     attrs: while (en.hasMore()) {
+//                             String v = en.next().toString();
+//                             if (v.equalsIgnoreCase(LdapObjs.top.name()))
+//                                     continue attrs;
+//                             if (first == null)
+//                                     first = v;
+//                             if (v.equalsIgnoreCase(getDirectory().getUserObjectClass()))
+//                                     primaryObjectClass = getDirectory().getUserObjectClass();
+//                             else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass()))
+//                                     primaryObjectClass = getDirectory().getGroupObjectClass();
+//                             objectClasses.add(v);
+//                     }
+//                     if (primaryObjectClass == null) {
+//                             if (first == null)
+//                                     throw new IllegalStateException("Could not find primary object class");
+//                             primaryObjectClass = first;
+//                     }
+//             } catch (NamingException e) {
+//                     throw new IllegalStateException("Cannot find object classes", e);
+//             }
+
+       }
+
+       @Override
+       public LdapName getDn() {
+               return dn;
+       }
+
+       public synchronized Attributes getAttributes() {
+               return isEditing() ? getModifiedAttributes() : getDirectory().getDirectoryDao().doGetAttributes(dn);
+       }
+
+       @Override
+       public List<LdapName> getReferences(String attributeId) {
+               Attribute memberAttribute = getAttributes().get(attributeId);
+               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 (NamingException e) {
+                       throw new IllegalStateException("Cannot get members", e);
+               }
+
+       }
+
+       /** Should only be called from working copy thread. */
+       protected synchronized Attributes getModifiedAttributes() {
+               assert getWc() != null;
+               return getWc().getModifiedData().get(getDn());
+       }
+
+       protected synchronized boolean isEditing() {
+               return getWc() != null && getModifiedAttributes() != null;
+       }
+
+       private synchronized LdapEntryWorkingCopy getWc() {
+               return directory.getWorkingCopy();
+       }
+
+       protected synchronized void startEditing() {
+//             if (frozen)
+//                     throw new IllegalStateException("Cannot edit frozen view");
+               if (directory.isReadOnly())
+                       throw new IllegalStateException("User directory is read-only");
+               assert getModifiedAttributes() == null;
+               getWc().startEditing(this);
+               // modifiedAttributes = (Attributes) publishedAttributes.clone();
+       }
+
+       public synchronized void publishAttributes(Attributes modifiedAttributes) {
+//             publishedAttributes = modifiedAttributes;
+       }
+
+       /*
+        * PROPERTIES
+        */
+       @Override
+       public Dictionary<String, Object> getProperties() {
+               if (properties == null)
+                       properties = new AttributeDictionary(false);
+               return properties;
+       }
+
+       public Dictionary<String, Object> getCredentials() {
+               if (credentials == null)
+                       credentials = new AttributeDictionary(true);
+               return credentials;
+       }
+
+       /*
+        * 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 = DirectoryDigestUtils.bytesToChars(value);
+
+                       if (getDirectory().getForcedPassword() != null
+                                       && getDirectory().getForcedPassword().equals(new String(password)))
+                               return true;
+
+                       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(), DirectoryDigestUtils.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 = DirectoryDigestUtils.bytesToChars((byte[]) value);
+                                       byte[] valueBytes;
+                                       if (DirectoryDigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
+                                               valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null,
+                                                               null);
+                                       } else if (DirectoryDigestUtils.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 = DirectoryDigestUtils.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 */
+       private static byte[] sha1hash(char[] password) {
+               byte[] hashedPassword = ("{SHA}" + Base64.getEncoder()
+                               .encodeToString(DirectoryDigestUtils.sha1(DirectoryDigestUtils.charsToBytes(password))))
+                               .getBytes(StandardCharsets.UTF_8);
+               return hashedPassword;
+       }
+
+       public AbstractLdapDirectory getDirectory() {
+               return directory;
+       }
+
+       public LdapDirectoryDao getDirectoryDao() {
+               return directory.getDirectoryDao();
+       }
+
+       @Override
+       public int hashCode() {
+               return dn.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (this == obj)
+                       return true;
+               if (obj instanceof LdapEntry) {
+                       LdapEntry that = (LdapEntry) obj;
+                       return this.dn.equals(that.getDn());
+               }
+               return false;
+       }
+
+       @Override
+       public String toString() {
+               return dn.toString();
+       }
+
+       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;
+       }
+
+       protected 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 credentials) {
+                       this.attrFilter = getDirectory().getCredentialAttributeIds();
+                       this.includeFilter = credentials;
+                       try {
+                               NamingEnumeration<String> ids = getAttributes().getIDs();
+                               while (ids.hasMore()) {
+                                       String id = ids.next();
+                                       if (credentials && attrFilter.contains(id))
+                                               effectiveKeys.add(id);
+                                       else if (!credentials && !attrFilter.contains(id))
+                                               effectiveKeys.add(id);
+                               }
+                       } catch (NamingException e) {
+                               throw new IllegalStateException("Cannot initialise attribute dictionary", e);
+                       }
+                       if (!credentials)
+                               effectiveKeys.add(LdapAttrs.objectClasses.name());
+               }
+
+               @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 = !key.equals(LdapAttrs.objectClasses.name()) ? getAttributes().get(key.toString())
+                                               : getAttributes().get(LdapAttrs.objectClass.name());
+                               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;
+                               // special case for object class
+                               if (key.equals(LdapAttrs.objectClass.name())) {
+                                       // TODO support multiple object classes
+                                       NamingEnumeration<?> en = attr.getAll();
+                                       String first = null;
+                                       attrs: while (en.hasMore()) {
+                                               String v = en.next().toString();
+                                               if (v.equalsIgnoreCase(LdapObjs.top.name()))
+                                                       continue attrs;
+                                               if (first == null)
+                                                       first = v;
+                                               if (v.equalsIgnoreCase(getDirectory().getUserObjectClass()))
+                                                       return getDirectory().getUserObjectClass();
+                                               else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass()))
+                                                       return getDirectory().getGroupObjectClass();
+                                       }
+                                       if (first != null)
+                                               return first;
+                                       throw new IllegalStateException("Cannot find objectClass in " + value);
+                               } else {
+                                       NamingEnumeration<?> en = attr.getAll();
+                                       StringJoiner values = new StringJoiner("\n");
+                                       while (en.hasMore()) {
+                                               String v = en.next().toString();
+                                               values.add(v);
+                                       }
+                                       return values.toString();
+                               }
+//                             else
+//                                     return value;
+                       } catch (NamingException e) {
+                               throw new IllegalStateException("Cannot get value for attribute " + key, e);
+                       }
+               }
+
+               @Override
+               public Object put(String key, Object value) {
+                       Objects.requireNonNull(value, "Value for key " + key + " is null");
+                       try {
+                               if (key == null) {
+                                       // FIXME remove this "feature", a key should be specified
+                                       // TODO persist to other sources (like PKCS12)
+                                       char[] password = DirectoryDigestUtils.bytesToChars(value);
+                                       byte[] hashedPassword = sha1hash(password);
+                                       return put(LdapAttrs.userPassword.name(), hashedPassword);
+                               }
+                               if (key.startsWith("X-")) {
+                                       return put(LdapAttrs.authPassword.name(), value);
+                               }
+
+                               // start editing
+                               getDirectory().checkEdit();
+                               if (!isEditing())
+                                       startEditing();
+
+                               // object classes special case.
+                               if (key.equals(LdapAttrs.objectClasses.name())) {
+                                       Attribute attribute = new BasicAttribute(LdapAttrs.objectClass.name());
+                                       String[] objectClasses = value.toString().split("\n");
+                                       for (String objectClass : objectClasses) {
+                                               if (objectClass.trim().equals(""))
+                                                       continue;
+                                               attribute.add(objectClass);
+                                       }
+                                       Attribute previousAttribute = getModifiedAttributes().put(attribute);
+                                       if (previousAttribute != null)
+                                               return previousAttribute.get();
+                                       else
+                                               return null;
+                               }
+
+                               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");
+
+                               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 IllegalStateException("Cannot get value for attribute " + key, e);
+                       }
+               }
+
+               @Override
+               public Object remove(Object key) {
+                       getDirectory().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 IllegalStateException("Cannot remove attribute " + key, e);
+                       }
+               }
+
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/IpaUtils.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/IpaUtils.java
new file mode 100644 (file)
index 0000000..a31cdac
--- /dev/null
@@ -0,0 +1,141 @@
+package org.argeo.cms.directory.ldap;
+
+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 java.util.StringJoiner;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.api.acr.ldap.LdapAttrs;
+import org.argeo.cms.dns.DnsBrowser;
+import org.argeo.cms.runtime.DirectoryConf;
+
+/** Free IPA specific conventions. */
+public class IpaUtils {
+       public final static String IPA_USER_BASE = "cn=users";
+       public final static String IPA_GROUP_BASE = "cn=groups";
+       public final static String IPA_ROLE_BASE = "cn=roles";
+       public final static String IPA_SERVICE_BASE = "cn=services";
+
+       public final static String IPA_ACCOUNTS_BASE = "cn=accounts";
+
+       private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase();
+
+       public final static String IPA_USER_DIRECTORY_CONFIG = DirectoryConf.userBase + "=" + IPA_USER_BASE + "&"
+                       + DirectoryConf.groupBase + "=" + IPA_GROUP_BASE + "&" + DirectoryConf.systemRoleBase + "=" + IPA_ROLE_BASE
+                       + "&" + DirectoryConf.readOnly + "=true";
+
+       @Deprecated
+       static String domainToUserDirectoryConfigPath(String realm) {
+               return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + DirectoryConf.realm.name() + "=" + realm;
+       }
+
+       public static void addIpaConfig(String realm, Dictionary<String, Object> properties) {
+               properties.put(DirectoryConf.baseDn.name(), domainToBaseDn(realm));
+               properties.put(DirectoryConf.realm.name(), realm);
+               properties.put(DirectoryConf.userBase.name(), IPA_USER_BASE);
+               properties.put(DirectoryConf.groupBase.name(), IPA_GROUP_BASE);
+               properties.put(DirectoryConf.systemRoleBase.name(), IPA_ROLE_BASE);
+               properties.put(DirectoryConf.readOnly.name(), Boolean.TRUE.toString());
+       }
+
+       public static String domainToBaseDn(String domain) {
+               String[] dcs = domain.split("\\.");
+               StringJoiner sj = new StringJoiner(",");
+               for (int i = 0; i < dcs.length; i++) {
+                       String dc = dcs[i];
+                       sj.add(LdapAttrs.dc.name() + '=' + dc.toLowerCase());
+               }
+               return IPA_ACCOUNTS_BASE + ',' + sj.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 (IOException e) {
+                       throw new IllegalStateException("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 IllegalStateException("No Kerberos domain available for " + uri);
+               // TODO intergrate CA certificate in truststore
+               // String schemeToUse = SCHEME_LDAPS;
+               String schemeToUse = DirectoryConf.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(DirectoryConf.SCHEME_LDAP) ? true : false);
+                               if (ldapHosts == null || ldapHosts.size() == 0) {
+                                       throw new IllegalStateException("Cannot configure LDAP for IPA " + uri);
+                               } else {
+                                       ldapHostsStr = ldapHosts.get(0);
+                               }
+                       } catch (IOException e) {
+                               throw new IllegalStateException("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 IllegalStateException("Cannot convert IPA uri " + uri, e);
+               }
+
+               Hashtable<String, Object> res = new Hashtable<>();
+               res.put(DirectoryConf.uri.name(), uriStr.toString());
+               addIpaConfig(kerberosRealm, res);
+               return res;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapConnection.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapConnection.java
new file mode 100644 (file)
index 0000000..100441c
--- /dev/null
@@ -0,0 +1,162 @@
+package org.argeo.cms.directory.ldap;
+
+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.api.acr.ldap.LdapAttrs;
+import org.argeo.api.cms.transaction.WorkingCopy;
+
+/** A synchronized wrapper for a single {@link InitialLdapContext}. */
+// TODO implement multiple contexts and connection pooling.
+public class LdapConnection {
+       private InitialLdapContext initialLdapContext = null;
+
+       public 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 (NamingException e) {
+                       throw new IllegalStateException("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);
+               }
+       }
+
+       public synchronized boolean entryExists(LdapName name) throws NamingException {
+               String[] noAttrOID = new String[] { "1.1" };
+               try {
+                       getLdapContext().getAttributes(name, noAttrOID);
+                       return true;
+               } catch (CommunicationException e) {
+                       reconnect();
+                       getLdapContext().getAttributes(name, noAttrOID);
+                       return true;
+               } catch (NameNotFoundException e) {
+                       return false;
+               }
+       }
+
+       public synchronized void prepareChanges(WorkingCopy<?, ?, LdapName> wc) throws NamingException {
+               // make sure connection will work
+               reconnect();
+
+               // delete
+               for (LdapName dn : wc.getDeletedData().keySet()) {
+                       if (!entryExists(dn))
+                               throw new IllegalStateException("User to delete no found " + dn);
+               }
+               // add
+               for (LdapName dn : wc.getNewData().keySet()) {
+                       if (entryExists(dn))
+                               throw new IllegalStateException("User to create found " + dn);
+               }
+               // modify
+               for (LdapName dn : wc.getModifiedData().keySet()) {
+                       if (!wc.getNewData().containsKey(dn) && !entryExists(dn))
+                               throw new IllegalStateException("User to modify not found " + dn);
+               }
+
+       }
+
+//     protected boolean entryExists(LdapName dn) throws NamingException {
+//             try {
+//                     return getAttributes(dn).size() != 0;
+//             } catch (NameNotFoundException e) {
+//                     return false;
+//             }
+//     }
+
+       public synchronized void commitChanges(LdapEntryWorkingCopy wc) throws NamingException {
+               // delete
+               for (LdapName dn : wc.getDeletedData().keySet()) {
+                       getLdapContext().destroySubcontext(dn);
+               }
+               // add
+               for (LdapName dn : wc.getNewData().keySet()) {
+                       LdapEntry user = wc.getNewData().get(dn);
+                       getLdapContext().createSubcontext(dn, user.getAttributes());
+               }
+               // modify
+               for (LdapName dn : wc.getModifiedData().keySet()) {
+                       Attributes modifiedAttrs = wc.getModifiedData().get(dn);
+                       getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs);
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDao.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDao.java
new file mode 100644 (file)
index 0000000..461013f
--- /dev/null
@@ -0,0 +1,264 @@
+package org.argeo.cms.directory.ldap;
+
+import static org.argeo.api.acr.ldap.LdapAttrs.objectClass;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.naming.AuthenticationNotSupportedException;
+import javax.naming.Binding;
+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.BasicAttributes;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.argeo.api.acr.ldap.LdapAttrs;
+import org.argeo.api.acr.ldap.LdapObjs;
+import org.argeo.api.cms.directory.HierarchyUnit;
+
+/** A user admin based on a LDAP server. */
+public class LdapDao extends AbstractLdapDirectoryDao {
+       private LdapConnection ldapConnection;
+
+       public LdapDao(AbstractLdapDirectory directory) {
+               super(directory);
+       }
+
+       @Override
+       public void init() {
+               ldapConnection = new LdapConnection(getDirectory().getUri().toString(), getDirectory().cloneConfigProperties());
+       }
+
+       public void destroy() {
+               ldapConnection.destroy();
+       }
+
+       @Override
+       public boolean checkConnection() {
+               try {
+                       return ldapConnection.entryExists(getDirectory().getBaseDn());
+               } catch (NamingException e) {
+                       return false;
+               }
+       }
+
+       @Override
+       public boolean entryExists(LdapName dn) {
+               try {
+                       return ldapConnection.entryExists(dn);
+               } catch (NameNotFoundException e) {
+                       return false;
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot check " + dn, e);
+               }
+       }
+
+       @Override
+       public LdapEntry doGetEntry(LdapName name) throws NameNotFoundException {
+//             if (!entryExists(name))
+//                     throw new NameNotFoundException(name + " was not found in " + getDirectory().getBaseDn());
+               try {
+                       Attributes attrs = ldapConnection.getAttributes(name);
+
+                       LdapEntry res;
+                       Rdn technicalRdn = LdapNameUtils.getParentRdn(name);
+                       if (getDirectory().getGroupBaseRdn().equals(technicalRdn)) {
+                               if (attrs.size() == 0) {// exists but not accessible
+                                       attrs = new BasicAttributes();
+                                       attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name());
+                                       attrs.put(LdapAttrs.objectClass.name(), getDirectory().getGroupObjectClass());
+                               }
+                               res = newGroup(name);
+                       } else if (getDirectory().getSystemRoleBaseRdn().equals(technicalRdn)) {
+                               if (attrs.size() == 0) {// exists but not accessible
+                                       attrs = new BasicAttributes();
+                                       attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name());
+                                       attrs.put(LdapAttrs.objectClass.name(), getDirectory().getGroupObjectClass());
+                               }
+                               res = newGroup(name);
+                       } else if (getDirectory().getUserBaseRdn().equals(technicalRdn)) {
+                               if (attrs.size() == 0) {// exists but not accessible
+                                       attrs = new BasicAttributes();
+                                       attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name());
+                                       attrs.put(LdapAttrs.objectClass.name(), getDirectory().getUserObjectClass());
+                               }
+                               res = newUser(name);
+                       } else {
+                               res = new DefaultLdapEntry(getDirectory(), name);
+                       }
+                       return res;
+               } catch (NameNotFoundException e) {
+                       throw e;
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot retrieve entry " + name, e);
+               }
+       }
+
+       @Override
+       public Attributes doGetAttributes(LdapName name) {
+               try {
+                       Attributes attrs = ldapConnection.getAttributes(name);
+                       return attrs;
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot get attributes for " + name);
+               }
+       }
+
+       @Override
+       public List<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
+               ArrayList<LdapEntry> res = new ArrayList<>();
+               try {
+                       String searchFilter = f != null ? f.toString()
+                                       : "(|(" + objectClass.name() + "=" + getDirectory().getUserObjectClass() + ")(" + objectClass.name()
+                                                       + "=" + getDirectory().getGroupObjectClass() + "))";
+                       SearchControls searchControls = new SearchControls();
+                       // only attribute needed is objectClass
+                       searchControls.setReturningAttributes(new String[] { objectClass.name() });
+                       // FIXME make one level consistent with deep
+                       searchControls.setSearchScope(deep ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_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);
+                               LdapEntry role;
+                               if (objectClassAttr.contains(getDirectory().getGroupObjectClass())
+                                               || objectClassAttr.contains(getDirectory().getGroupObjectClass().toLowerCase()))
+                                       role = newGroup(dn);
+                               else if (objectClassAttr.contains(getDirectory().getUserObjectClass())
+                                               || objectClassAttr.contains(getDirectory().getUserObjectClass().toLowerCase()))
+                                       role = newUser(dn);
+                               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 (NamingException e) {
+                       throw new IllegalStateException("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
+       public List<LdapName> getDirectGroups(LdapName dn) {
+               List<LdapName> directGroups = new ArrayList<LdapName>();
+               try {
+                       String searchFilter = "(&(" + objectClass + "=" + getDirectory().getGroupObjectClass() + ")("
+                                       + getDirectory().getMemberAttributeId() + "=" + dn + "))";
+
+                       SearchControls searchControls = new SearchControls();
+                       searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+
+                       LdapName searchBase = getDirectory().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 (NamingException e) {
+                       throw new IllegalStateException("Cannot populate direct members of " + dn, e);
+               }
+       }
+
+       @Override
+       public void prepare(LdapEntryWorkingCopy wc) {
+               try {
+                       ldapConnection.prepareChanges(wc);
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot prepare LDAP", e);
+               }
+       }
+
+       @Override
+       public void commit(LdapEntryWorkingCopy wc) {
+               try {
+                       ldapConnection.commitChanges(wc);
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot commit LDAP", e);
+               }
+       }
+
+       @Override
+       public void rollback(LdapEntryWorkingCopy wc) {
+               // prepare not impacting
+       }
+
+       /*
+        * HIERARCHY
+        */
+
+       @Override
+       public Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
+               List<HierarchyUnit> res = new ArrayList<>();
+               try {
+                       String structuralFilter = functionalOnly ? ""
+                                       : "(" + getDirectory().getUserBaseRdn() + ")(" + getDirectory().getGroupBaseRdn() + ")("
+                                                       + getDirectory().getSystemRoleBaseRdn() + ")";
+                       String searchFilter = "(|(" + objectClass + "=" + LdapObjs.organizationalUnit.name() + ")(" + objectClass
+                                       + "=" + LdapObjs.organization.name() + ")" + structuralFilter + ")";
+
+                       SearchControls searchControls = new SearchControls();
+                       searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+                       // no attributes needed
+                       searchControls.setReturningAttributes(new String[0]);
+
+                       NamingEnumeration<SearchResult> results = ldapConnection.search(searchBase, searchFilter, searchControls);
+
+                       while (results.hasMoreElements()) {
+                               SearchResult searchResult = (SearchResult) results.nextElement();
+                               LdapName dn = toDn(searchBase, searchResult);
+//                             Attributes attrs = searchResult.getAttributes();
+                               LdapHierarchyUnit hierarchyUnit = new LdapHierarchyUnit(getDirectory(), dn);
+                               if (functionalOnly) {
+                                       if (hierarchyUnit.isFunctional())
+                                               res.add(hierarchyUnit);
+                               } else {
+                                       res.add(hierarchyUnit);
+                               }
+                       }
+                       return res;
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot get direct hierarchy units ", e);
+               }
+       }
+
+       @Override
+       public HierarchyUnit doGetHierarchyUnit(LdapName dn) {
+               try {
+                       if (getDirectory().getBaseDn().equals(dn))
+                               return getDirectory();
+                       if (!dn.startsWith(getDirectory().getBaseDn()))
+                               throw new IllegalArgumentException(dn + " does not start with base DN " + getDirectory().getBaseDn());
+                       if (!ldapConnection.entryExists(dn))
+                               return null;
+                       return new LdapHierarchyUnit(getDirectory(), dn);
+               } catch (NameNotFoundException e) {
+                       return null;
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot get hierarchy unit " + dn, e);
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDirectoryDao.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapDirectoryDao.java
new file mode 100644 (file)
index 0000000..03b03ea
--- /dev/null
@@ -0,0 +1,37 @@
+package org.argeo.cms.directory.ldap;
+
+import java.util.List;
+
+import javax.naming.NameNotFoundException;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.api.cms.directory.HierarchyUnit;
+import org.argeo.api.cms.transaction.WorkingCopyProcessor;
+
+/** Low-level access to an LDAP/LDIF directory. */
+public interface LdapDirectoryDao extends WorkingCopyProcessor<LdapEntryWorkingCopy> {
+       boolean checkConnection();
+
+       boolean entryExists(LdapName dn);
+
+       LdapEntry doGetEntry(LdapName name) throws NameNotFoundException;
+
+       Attributes doGetAttributes(LdapName name);
+
+       List<LdapEntry> doGetEntries(LdapName searchBase, String filter, boolean deep);
+
+       List<LdapName> getDirectGroups(LdapName dn);
+
+       Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly);
+
+       HierarchyUnit doGetHierarchyUnit(LdapName dn);
+
+       LdapEntry newUser(LdapName name);
+
+       LdapEntry newGroup(LdapName name);
+
+       void init();
+
+       void destroy();
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntry.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntry.java
new file mode 100644 (file)
index 0000000..e258762
--- /dev/null
@@ -0,0 +1,65 @@
+package org.argeo.cms.directory.ldap;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.TreeSet;
+
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.api.acr.ldap.LdapAttrs;
+
+/** An LDAP entry. */
+public interface LdapEntry {
+       LdapName getDn();
+
+       Attributes getAttributes();
+
+       void publishAttributes(Attributes modifiedAttributes);
+
+       List<LdapName> getReferences(String attributeId);
+
+       Dictionary<String, Object> getProperties();
+
+       boolean hasCredential(String key, Object value);
+
+       /*
+        * UTILITIES
+        */
+       /**
+        * Convert a collection of object classes to the format expected by an LDAP
+        * backend.
+        */
+       public static void addObjectClasses(Dictionary<String, Object> properties, Collection<String> objectClasses) {
+               String value = properties.get(LdapAttrs.objectClasses.name()).toString();
+               Set<String> currentObjectClasses = new TreeSet<>(Arrays.asList(value.toString().split("\n")));
+               currentObjectClasses.addAll(objectClasses);
+               StringJoiner values = new StringJoiner("\n");
+               currentObjectClasses.forEach((s) -> values.add(s));
+               properties.put(LdapAttrs.objectClasses.name(), values.toString());
+       }
+
+       public static Object getLocalized(Dictionary<String, Object> properties, String key, Locale locale) {
+               if (locale == null)
+                       return null;
+               Object value = null;
+               value = properties.get(key + ";lang-" + locale.getLanguage() + "-" + locale.getCountry());
+               if (value == null)
+                       value = properties.get(key + ";lang-" + locale.getLanguage());
+               return value;
+       }
+
+       public static String toLocalizedKey(String key, Locale locale) {
+               String country = locale.getCountry();
+               if ("".equals(country)) {
+                       return key + ";lang-" + locale.getLanguage();
+               } else {
+                       return key + ";lang-" + locale.getLanguage() + "-" + locale.getCountry();
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntryWorkingCopy.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapEntryWorkingCopy.java
new file mode 100644 (file)
index 0000000..b5afc9d
--- /dev/null
@@ -0,0 +1,19 @@
+package org.argeo.cms.directory.ldap;
+
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.api.cms.transaction.AbstractWorkingCopy;
+
+/** Working copy for a user directory being edited. */
+public class LdapEntryWorkingCopy extends AbstractWorkingCopy<LdapEntry, Attributes, LdapName> {
+       @Override
+       protected LdapName getId(LdapEntry entry) {
+               return entry.getDn();
+       }
+
+       @Override
+       protected Attributes cloneAttributes(LdapEntry entry) {
+               return (Attributes) entry.getAttributes().clone();
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapHierarchyUnit.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapHierarchyUnit.java
new file mode 100644 (file)
index 0000000..7abf098
--- /dev/null
@@ -0,0 +1,64 @@
+package org.argeo.cms.directory.ldap;
+
+import java.util.Locale;
+
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.argeo.api.cms.directory.HierarchyUnit;
+
+/** LDIF/LDAP based implementation of {@link HierarchyUnit}. */
+public class LdapHierarchyUnit extends DefaultLdapEntry implements HierarchyUnit {
+       private final boolean functional;
+
+       public LdapHierarchyUnit(AbstractLdapDirectory directory, LdapName dn) {
+               super(directory, dn);
+
+               Rdn rdn = LdapNameUtils.getLastRdn(dn);
+               functional = !(directory.getUserBaseRdn().equals(rdn) || directory.getGroupBaseRdn().equals(rdn)
+                               || directory.getSystemRoleBaseRdn().equals(rdn));
+       }
+
+       @Override
+       public HierarchyUnit getParent() {
+               return getDirectoryDao().doGetHierarchyUnit(LdapNameUtils.getParent(getDn()));
+       }
+
+       @Override
+       public Iterable<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly) {
+               return getDirectoryDao().doGetDirectHierarchyUnits(getDn(), functionalOnly);
+       }
+
+       @Override
+       public boolean isFunctional() {
+               return functional;
+       }
+
+       @Override
+       public String getHierarchyUnitName() {
+               String name = LdapNameUtils.getLastRdnValue(getDn());
+               // TODO check ou, o, etc.
+               return name;
+       }
+
+       @Override
+       public String getHierarchyUnitLabel(Locale locale) {
+               String key = LdapNameUtils.getLastRdn(getDn()).getType();
+               Object value = LdapEntry.getLocalized(getProperties(), key, locale);
+               if (value == null)
+                       value = getHierarchyUnitName();
+               assert value != null;
+               return value.toString();
+       }
+
+       @Override
+       public String getBase() {
+               return getDn().toString();
+       }
+
+       @Override
+       public String toString() {
+               return "Hierarchy Unit " + getDn().toString();
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapNameUtils.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdapNameUtils.java
new file mode 100644 (file)
index 0000000..74f23da
--- /dev/null
@@ -0,0 +1,69 @@
+package org.argeo.cms.directory.ldap;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+/** Utilities to simplify using {@link LdapName}. */
+public class LdapNameUtils {
+
+       public static LdapName relativeName(LdapName prefix, LdapName dn) {
+               try {
+                       if (!dn.startsWith(prefix))
+                               throw new IllegalArgumentException("Prefix " + prefix + " not consistent with " + dn);
+                       LdapName res = (LdapName) dn.clone();
+                       for (int i = 0; i < prefix.size(); i++) {
+                               res.remove(0);
+                       }
+                       return res;
+               } catch (InvalidNameException e) {
+                       throw new IllegalStateException("Cannot find realtive name", e);
+               }
+       }
+
+       public static LdapName getParent(LdapName dn) {
+               try {
+                       LdapName parent = (LdapName) dn.clone();
+                       parent.remove(parent.size() - 1);
+                       return parent;
+               } catch (InvalidNameException e) {
+                       throw new IllegalArgumentException("Cannot get parent of " + dn, e);
+               }
+       }
+
+       public static Rdn getParentRdn(LdapName dn) {
+               if (dn.size() < 2)
+                       throw new IllegalArgumentException(dn + " has no parent");
+               Rdn parentRdn = dn.getRdn(dn.size() - 2);
+               return parentRdn;
+       }
+
+       public static LdapName toLdapName(String distinguishedName) {
+               try {
+                       return new LdapName(distinguishedName);
+               } catch (InvalidNameException e) {
+                       throw new IllegalArgumentException("Cannot parse " + distinguishedName + " as LDAP name", e);
+               }
+       }
+
+       public static Rdn getLastRdn(LdapName dn) {
+               return dn.getRdn(dn.size() - 1);
+       }
+
+       public static String getLastRdnAsString(LdapName dn) {
+               return getLastRdn(dn).toString();
+       }
+
+       public static String getLastRdnValue(String dn) {
+               return getLastRdnValue(toLdapName(dn));
+       }
+
+       public static String getLastRdnValue(LdapName dn) {
+               return getLastRdn(dn).getValue().toString();
+       }
+
+       /** singleton */
+       private LdapNameUtils() {
+
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifDao.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifDao.java
new file mode 100644 (file)
index 0000000..1f33898
--- /dev/null
@@ -0,0 +1,306 @@
+package org.argeo.cms.directory.ldap;
+
+import static org.argeo.api.acr.ldap.LdapAttrs.objectClass;
+import static org.argeo.api.acr.ldap.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.HashSet;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.api.acr.ldap.LdapObjs;
+import org.argeo.api.cms.directory.HierarchyUnit;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+
+/** A user admin based on a LDIF files. */
+public class LdifDao extends AbstractLdapDirectoryDao {
+       private NavigableMap<LdapName, LdapEntry> entries = new TreeMap<>();
+       private NavigableMap<LdapName, LdapHierarchyUnit> hierarchy = new TreeMap<>();
+
+       private NavigableMap<LdapName, Attributes> values = new TreeMap<>();
+
+       public LdifDao(AbstractLdapDirectory directory) {
+               super(directory);
+       }
+
+       public void init() {
+               String uri = getDirectory().getUri();
+               if (uri == null)
+                       return;
+               try {
+                       URI u = new URI(uri);
+                       if (u.getScheme().equals("file")) {
+                               File file = new File(u);
+                               if (!file.exists())
+                                       return;
+                       }
+                       load(u.toURL().openStream());
+               } catch (IOException | URISyntaxException e) {
+                       throw new IllegalStateException("Cannot open URL " + getDirectory().getUri(), e);
+               }
+       }
+
+       public void save() {
+               if (getDirectory().getUri() == null)
+                       return; // ignore
+               if (getDirectory().isReadOnly())
+                       throw new IllegalStateException(
+                                       "Cannot save LDIF user admin: " + getDirectory().getUri() + " is read-only");
+               try (FileOutputStream out = new FileOutputStream(new File(new URI(getDirectory().getUri())))) {
+                       save(out);
+               } catch (IOException | URISyntaxException e) {
+                       throw new IllegalStateException("Cannot save user admin to " + getDirectory().getUri(), e);
+               }
+       }
+
+       public void save(OutputStream out) throws IOException {
+               try {
+                       LdifWriter ldifWriter = new LdifWriter(out);
+                       for (LdapName name : hierarchy.keySet())
+                               ldifWriter.writeEntry(name, hierarchy.get(name).getAttributes());
+                       for (LdapName name : entries.keySet())
+                               ldifWriter.writeEntry(name, entries.get(name).getAttributes());
+               } finally {
+                       out.close();
+               }
+       }
+
+       public void load(InputStream in) {
+               try {
+                       entries.clear();
+                       hierarchy.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 IllegalStateException(key + " has duplicate id " + id);
+                                       lowerCase.add(id);
+                               }
+
+                               values.put(key, attributes);
+
+                               // 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())) {
+                                               entries.put(key, newUser(key));
+                                               break objectClasses;
+                                       } else if (objectClass.toLowerCase().equals(getDirectory().getGroupObjectClass().toLowerCase())) {
+                                               entries.put(key, newGroup(key));
+                                               break objectClasses;
+                                       } else if (objectClass.equalsIgnoreCase(LdapObjs.organizationalUnit.name())) {
+                                               // TODO skip if it does not contain groups or users
+                                               hierarchy.put(key, new LdapHierarchyUnit(getDirectory(), key));
+                                               break objectClasses;
+                                       }
+                               }
+                       }
+
+               } catch (NamingException | IOException e) {
+                       throw new IllegalStateException("Cannot load user admin service from LDIF", e);
+               }
+       }
+
+       public void destroy() {
+//             if (users == null || groups == null)
+               if (entries == null)
+                       throw new IllegalStateException("User directory " + getDirectory().getBaseDn() + " is already destroyed");
+//             users = null;
+//             groups = null;
+               entries = null;
+       }
+
+       /*
+        * USER ADMIN
+        */
+
+       @Override
+       public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException {
+               if (entries.containsKey(key))
+                       return entries.get(key);
+               throw new NameNotFoundException(key + " not persisted");
+       }
+
+       @Override
+       public Attributes doGetAttributes(LdapName name) {
+               if (!values.containsKey(name))
+                       throw new IllegalStateException(name + " doe not exist in " + getDirectory().getBaseDn());
+               return values.get(name);
+       }
+
+       @Override
+       public boolean checkConnection() {
+               return true;
+       }
+
+       @Override
+       public boolean entryExists(LdapName dn) {
+               return entries.containsKey(dn);// || groups.containsKey(dn);
+       }
+
+       @Override
+       public List<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
+               Objects.requireNonNull(searchBase);
+               ArrayList<LdapEntry> res = new ArrayList<>();
+               if (f == null && deep && getDirectory().getBaseDn().equals(searchBase)) {
+                       res.addAll(entries.values());
+               } else {
+                       filterRoles(entries, searchBase, f, deep, res);
+               }
+               return res;
+       }
+
+       private void filterRoles(SortedMap<LdapName, ? extends LdapEntry> map, LdapName searchBase, String f, boolean deep,
+                       List<LdapEntry> res) {
+               // FIXME get rid of OSGi references
+               try {
+                       // TODO reduce map with search base ?
+                       Filter filter = f != null ? FrameworkUtil.createFilter(f) : null;
+                       roles: for (LdapEntry user : map.values()) {
+                               LdapName dn = user.getDn();
+                               if (dn.startsWith(searchBase)) {
+                                       if (!deep && dn.size() != (searchBase.size() + 1))
+                                               continue roles;
+                                       if (filter == null)
+                                               res.add(user);
+                                       else {
+                                               if (user instanceof Role) {
+                                                       if (filter.match(((Role) user).getProperties()))
+                                                               res.add(user);
+                                               }
+                                       }
+                               }
+                       }
+               } catch (InvalidSyntaxException e) {
+                       throw new IllegalArgumentException("Cannot create filter " + f, e);
+               }
+
+       }
+
+       @Override
+       public List<LdapName> getDirectGroups(LdapName dn) {
+               List<LdapName> directGroups = new ArrayList<LdapName>();
+               entries: for (LdapName name : entries.keySet()) {
+                       LdapEntry group;
+                       try {
+                               LdapEntry entry = doGetEntry(name);
+                               if (AbstractLdapDirectory.hasObjectClass(entry.getAttributes(), getDirectory().getGroupObjectClass())) {
+                                       group = entry;
+                               } else {
+                                       continue entries;
+                               }
+                       } catch (NameNotFoundException e) {
+                               throw new IllegalArgumentException("Group " + dn + " not found", e);
+                       }
+                       if (group.getReferences(getDirectory().getMemberAttributeId()).contains(dn)) {
+                               directGroups.add(group.getDn());
+                       }
+               }
+               return directGroups;
+       }
+
+       @Override
+       public void prepare(LdapEntryWorkingCopy wc) {
+               // delete
+               for (LdapName dn : wc.getDeletedData().keySet()) {
+                       if (entries.containsKey(dn))
+                               entries.remove(dn);
+                       else
+                               throw new IllegalStateException("User to delete not found " + dn);
+               }
+               // add
+               for (LdapName dn : wc.getNewData().keySet()) {
+                       LdapEntry user = (LdapEntry) wc.getNewData().get(dn);
+                       if (entries.containsKey(dn))
+                               throw new IllegalStateException("User to create found " + dn);
+                       entries.put(dn, user);
+               }
+               // modify
+               for (LdapName dn : wc.getModifiedData().keySet()) {
+                       Attributes modifiedAttrs = wc.getModifiedData().get(dn);
+                       LdapEntry user;
+                       try {
+                               user = doGetEntry(dn);
+                       } catch (NameNotFoundException e) {
+                               throw new IllegalStateException("User to modify no found " + dn, e);
+                       }
+                       if (user == null)
+                               throw new IllegalStateException("User to modify no found " + dn);
+                       user.publishAttributes(modifiedAttrs);
+               }
+       }
+
+       @Override
+       public void commit(LdapEntryWorkingCopy wc) {
+               save();
+       }
+
+       @Override
+       public void rollback(LdapEntryWorkingCopy wc) {
+               init();
+       }
+
+       /*
+        * HIERARCHY
+        */
+       @Override
+       public HierarchyUnit doGetHierarchyUnit(LdapName dn) {
+               if (getDirectory().getBaseDn().equals(dn))
+                       return getDirectory();
+               return hierarchy.get(dn);
+       }
+
+       @Override
+       public Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
+               List<HierarchyUnit> res = new ArrayList<>();
+               for (LdapName n : hierarchy.keySet()) {
+                       if (n.size() == searchBase.size() + 1) {
+                               if (n.startsWith(searchBase)) {
+                                       HierarchyUnit hu = hierarchy.get(n);
+                                       if (functionalOnly) {
+                                               if (hu.isFunctional())
+                                                       res.add(hu);
+                                       } else {
+                                               res.add(hu);
+                                       }
+                               }
+                       }
+               }
+               return res;
+       }
+
+       public void scope(LdifDao scoped) {
+               scoped.entries = Collections.unmodifiableNavigableMap(entries);
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifParser.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifParser.java
new file mode 100644 (file)
index 0000000..c76c362
--- /dev/null
@@ -0,0 +1,161 @@
+package org.argeo.cms.directory.ldap;
+
+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.api.acr.ldap.LdapAttrs;
+
+/** 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 IllegalStateException(
+                                               "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 IllegalStateException("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
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifWriter.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/LdifWriter.java
new file mode 100644 (file)
index 0000000..2a1ed81
--- /dev/null
@@ -0,0 +1,104 @@
+package org.argeo.cms.directory.ldap;
+
+import static org.argeo.api.acr.ldap.LdapAttrs.DN;
+import static org.argeo.api.acr.ldap.LdapAttrs.member;
+import static org.argeo.api.acr.ldap.LdapAttrs.objectClass;
+import static org.argeo.api.acr.ldap.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;
+
+/** 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 IllegalArgumentException(
+                                               "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 IllegalStateException("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');
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/directory/ldap/SharedSecret.java b/org.argeo.cms/src/org/argeo/cms/directory/ldap/SharedSecret.java
new file mode 100644 (file)
index 0000000..2c52ee1
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.cms.directory.ldap;
+
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+
+import org.argeo.api.acr.ldap.NamingUtils;
+
+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());
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/dns/DnsBrowser.java b/org.argeo.cms/src/org/argeo/cms/dns/DnsBrowser.java
new file mode 100644 (file)
index 0000000..c6b6530
--- /dev/null
@@ -0,0 +1,216 @@
+package org.argeo.cms.dns;
+
+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.Objects;
+import java.util.SortedSet;
+import java.util.StringJoiner;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import javax.naming.Binding;
+import javax.naming.Context;
+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.DirContext;
+import javax.naming.directory.InitialDirContext;
+
+public class DnsBrowser implements Closeable {
+       private final DirContext initialCtx;
+
+       public DnsBrowser() {
+               this(new ArrayList<>());
+       }
+
+       public DnsBrowser(List<String> dnsServerUrls) {
+               try {
+                       Objects.requireNonNull(dnsServerUrls);
+                       Hashtable<String, Object> env = new Hashtable<>();
+                       env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
+                       if (!dnsServerUrls.isEmpty()) {
+                               boolean specified = false;
+                               StringJoiner providerUrl = new StringJoiner(" ");
+                               for (String dnsUrl : dnsServerUrls) {
+                                       if (dnsUrl != null) {
+                                               providerUrl.add(dnsUrl);
+                                               specified = true;
+                                       }
+                               }
+                               if (specified)
+                                       env.put(Context.PROVIDER_URL, providerUrl.toString());
+                       }
+                       initialCtx = new InitialDirContext(env);
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot initialise DNS borowser.", e);
+               }
+       }
+
+       public Map<String, List<String>> getAllRecords(String name) {
+               try {
+                       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);
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot get allrecords of " + name, e);
+               }
+       }
+
+       /**
+        * 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) {
+               try {
+                       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();
+               } catch (NameNotFoundException e) {
+                       return null;
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot get DNS entry " + recordType + " of " + name, e);
+               }
+       }
+
+       /**
+        * Return records of a given type.
+        */
+       public List<String> getRecords(String name, String recordType) {
+               try {
+                       List<String> res = new ArrayList<String>();
+                       Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType });
+                       Attribute attr = attrs.get(recordType);
+                       addValues(attr, res);
+                       return res;
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot get records " + recordType + " of " + name, e);
+               }
+       }
+
+       /** Ordered, with preferred first. */
+       public List<String> getSrvRecordsAsHosts(String name, boolean withPort) {
+               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) {
+               try {
+                       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);
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot list entries of " + name, e);
+               }
+       }
+
+       @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
diff --git a/org.argeo.cms/src/org/argeo/cms/dns/SrvRecord.java b/org.argeo.cms/src/org/argeo/cms/dns/SrvRecord.java
new file mode 100644 (file)
index 0000000..bdbdc76
--- /dev/null
@@ -0,0 +1,52 @@
+package org.argeo.cms.dns;
+
+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 : "");
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/file/ChecksumFactory.java b/org.argeo.cms/src/org/argeo/cms/file/ChecksumFactory.java
new file mode 100644 (file)
index 0000000..6aea8be
--- /dev/null
@@ -0,0 +1,149 @@
+package org.argeo.cms.file;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.zip.Checksum;
+
+/** Allows to fine tune how files are read. */
+public class ChecksumFactory {
+       private int regionSize = 10 * 1024 * 1024;
+
+       public byte[] digest(Path path, final String algo) {
+               try {
+                       final MessageDigest md = MessageDigest.getInstance(algo);
+                       if (Files.isDirectory(path)) {
+                               long begin = System.currentTimeMillis();
+                               Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
+
+                                       @Override
+                                       public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                               if (!Files.isDirectory(file)) {
+                                                       byte[] digest = digest(file, algo);
+                                                       md.update(digest);
+                                               }
+                                               return FileVisitResult.CONTINUE;
+                                       }
+
+                               });
+                               byte[] digest = md.digest();
+                               long duration = System.currentTimeMillis() - begin;
+                               System.out.println(printBase64Binary(digest) + " " + path + " (" + duration / 1000 + "s)");
+                               return digest;
+                       } else {
+                               long begin = System.nanoTime();
+                               long length = -1;
+                               try (FileChannel channel = (FileChannel) Files.newByteChannel(path);) {
+                                       length = channel.size();
+                                       long cursor = 0;
+                                       while (cursor < length) {
+                                               long effectiveSize = Math.min(regionSize, length - cursor);
+                                               MappedByteBuffer mb = channel.map(FileChannel.MapMode.READ_ONLY, cursor, effectiveSize);
+                                               // md.update(mb);
+                                               byte[] buffer = new byte[1024];
+                                               while (mb.hasRemaining()) {
+                                                       mb.get(buffer);
+                                                       md.update(buffer);
+                                               }
+
+                                               // sub digest
+                                               // mb.flip();
+                                               // MessageDigest subMd =
+                                               // MessageDigest.getInstance(algo);
+                                               // subMd.update(mb);
+                                               // byte[] subDigest = subMd.digest();
+                                               // System.out.println(" -> " + cursor);
+                                               // System.out.println(IOUtils.encodeHexString(subDigest));
+                                               // System.out.println(new BigInteger(1,
+                                               // subDigest).toString(16));
+                                               // System.out.println(new BigInteger(1, subDigest)
+                                               // .toString(Character.MAX_RADIX));
+                                               // System.out.println(printBase64Binary(subDigest));
+
+                                               cursor = cursor + regionSize;
+                                       }
+                                       byte[] digest = md.digest();
+                                       long duration = System.nanoTime() - begin;
+                                       System.out.println(printBase64Binary(digest) + " " + path.getFileName() + " (" + duration / 1000000
+                                                       + "ms, " + (length / 1024) + "kB, " + (length / (duration / 1000000)) * 1000 / (1024 * 1024)
+                                                       + " MB/s)");
+                                       return digest;
+                               }
+                       }
+               } catch (NoSuchAlgorithmException | IOException e) {
+                       throw new IllegalStateException("Cannot digest " + path, e);
+               }
+       }
+
+       /** Whether the file should be mapped. */
+       protected boolean mapFile(FileChannel fileChannel) throws IOException {
+               long size = fileChannel.size();
+               if (size > (regionSize / 10))
+                       return true;
+               return false;
+       }
+
+       public long checksum(Path path, Checksum crc) {
+               final int bufferSize = 2 * 1024 * 1024;
+               long begin = System.currentTimeMillis();
+               try (FileChannel channel = (FileChannel) Files.newByteChannel(path);) {
+                       byte[] bytes = new byte[bufferSize];
+                       long length = channel.size();
+                       long cursor = 0;
+                       while (cursor < length) {
+                               long effectiveSize = Math.min(regionSize, length - cursor);
+                               MappedByteBuffer mb = channel.map(FileChannel.MapMode.READ_ONLY, cursor, effectiveSize);
+                               int nGet;
+                               while (mb.hasRemaining()) {
+                                       nGet = Math.min(mb.remaining(), bufferSize);
+                                       mb.get(bytes, 0, nGet);
+                                       crc.update(bytes, 0, nGet);
+                               }
+                               cursor = cursor + regionSize;
+                       }
+                       return crc.getValue();
+               } catch (IOException e) {
+                       throw new IllegalStateException("Cannot checksum " + path, e);
+               } finally {
+                       long duration = System.currentTimeMillis() - begin;
+                       System.out.println(duration / 1000 + "s");
+               }
+       }
+
+       public static void main(String... args) {
+               ChecksumFactory cf = new ChecksumFactory();
+               // Path path =
+               // Paths.get("/home/mbaudier/apache-maven-3.2.3-bin.tar.gz");
+               Path path;
+               if (args.length > 0) {
+                       path = Paths.get(args[0]);
+               } else {
+                       path = Paths.get("/home/mbaudier/Downloads/torrents/CentOS-7-x86_64-DVD-1503-01/"
+                                       + "CentOS-7-x86_64-DVD-1503-01.iso");
+               }
+               // long adler = cf.checksum(path, new Adler32());
+               // System.out.format("Adler=%d%n", adler);
+               // long crc = cf.checksum(path, new CRC32());
+               // System.out.format("CRC=%d%n", crc);
+               String algo = "SHA1";
+               byte[] digest = cf.digest(path, algo);
+               System.out.println(algo + " " + printBase64Binary(digest));
+               System.out.println(algo + " " + new BigInteger(1, digest).toString(16));
+               // String sha1 = printBase64Binary(cf.digest(path, "SHA1"));
+               // System.out.format("SHA1=%s%n", sha1);
+       }
+
+       private static String printBase64Binary(byte[] arr) {
+               return Base64.getEncoder().encodeToString(arr);
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java b/org.argeo.cms/src/org/argeo/cms/http/HttpHeader.java
new file mode 100644 (file)
index 0000000..217b7a4
--- /dev/null
@@ -0,0 +1,33 @@
+package org.argeo.cms.http;
+
+/** Standard HTTP headers (including WebDav). */
+public enum HttpHeader {
+       AUTHORIZATION("Authorization"), //
+       WWW_AUTHENTICATE("WWW-Authenticate"), //
+       ALLOW("Allow"), //
+
+       // WebDav
+       DAV("DAV"), //
+       DEPTH("Depth"), //
+       ;
+
+       public final static String BASIC = "Basic";
+       public final static String REALM = "realm";
+       public final static String NEGOTIATE = "Negotiate";
+
+       private final String name;
+
+       private HttpHeader(String headerName) {
+               this.name = headerName;
+       }
+
+       public String getHeaderName() {
+               return name;
+       }
+
+       @Override
+       public String toString() {
+               return getHeaderName();
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpMethod.java b/org.argeo.cms/src/org/argeo/cms/http/HttpMethod.java
new file mode 100644 (file)
index 0000000..7869045
--- /dev/null
@@ -0,0 +1,19 @@
+package org.argeo.cms.http;
+
+/** Generic HTTP methods. */
+public enum HttpMethod {
+       OPTIONS, //
+       HEAD, //
+       GET, //
+       POST, //
+       PUT, //
+       DELETE, //
+
+       // WebDav
+       PROPFIND, //
+       PROPPATCH, //
+       MKCOL, //
+       MOVE, //
+       COPY, //
+       ;
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpServerUtils.java b/org.argeo.cms/src/org/argeo/cms/http/HttpServerUtils.java
new file mode 100644 (file)
index 0000000..fc04fbf
--- /dev/null
@@ -0,0 +1,45 @@
+package org.argeo.cms.http;
+
+import java.net.URI;
+import java.util.Objects;
+
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+
+public class HttpServerUtils {
+       private final static String SLASH = "/";
+
+       private static String extractPathWithingContext(HttpContext httpContext, String fullPath, boolean startWithSlash) {
+               Objects.requireNonNull(fullPath);
+               String contextPath = httpContext.getPath();
+               if (!fullPath.startsWith(contextPath))
+                       throw new IllegalArgumentException(fullPath + " does not belong to context" + contextPath);
+               String path = fullPath.substring(contextPath.length());
+               // TODO optimise?
+               if (!startWithSlash && path.startsWith(SLASH)) {
+                       path = path.substring(1);
+               } else if (startWithSlash && !path.startsWith(SLASH)) {
+                       path = SLASH + path;
+               }
+               return path;
+       }
+
+       /** Path within the context, NOT starting with a slash. */
+       public static String relativize(HttpExchange exchange) {
+               URI uri = exchange.getRequestURI();
+               HttpContext httpContext = exchange.getHttpContext();
+               return extractPathWithingContext(httpContext, uri.getPath(), false);
+       }
+
+       /** Path within the context, starting with a slash. */
+       public static String subPath(HttpExchange exchange) {
+               URI uri = exchange.getRequestURI();
+               HttpContext httpContext = exchange.getHttpContext();
+               return extractPathWithingContext(httpContext, uri.getPath(), true);
+       }
+
+       /** singleton */
+       private HttpServerUtils() {
+
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/http/HttpStatus.java b/org.argeo.cms/src/org/argeo/cms/http/HttpStatus.java
new file mode 100644 (file)
index 0000000..3b9a47a
--- /dev/null
@@ -0,0 +1,66 @@
+package org.argeo.cms.http;
+
+/**
+ * Standard HTTP response status codes (including WebDav ones).
+ * 
+ * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status"
+ */
+public enum HttpStatus {
+       // Successful responses (200–299)
+       OK(200, "OK"), //
+       NO_CONTENT(204, "No Content"), //
+       MULTI_STATUS(207, "Multi-Status"), // WebDav
+       // Client error responses (400–499)
+       UNAUTHORIZED(401, "Unauthorized"), //
+       FORBIDDEN(403, "Forbidden"), //
+       NOT_FOUND(404, "Not Found"), //
+       // Server error responses (500-599)
+       INTERNAL_SERVER_ERROR(500, "Internal Server Error"), //
+       NOT_IMPLEMENTED(501, "Not Implemented"), //
+       ;
+
+       private final int code;
+       private final String reasonPhrase;
+
+       HttpStatus(int statusCode, String reasonPhrase) {
+               this.code = statusCode;
+               this.reasonPhrase = reasonPhrase;
+       }
+
+       public int getCode() {
+               return code;
+       }
+
+       public String getReasonPhrase() {
+               return reasonPhrase;
+       }
+
+       /**
+        * The status line, as defined by RFC2616.
+        * 
+        * @see "https://www.rfc-editor.org/rfc/rfc2616#section-6.1"
+        */
+       public String getStatusLine(String httpVersion) {
+               return httpVersion + " " + code + " " + reasonPhrase;
+       }
+
+       public static HttpStatus parseStatusLine(String statusLine) {
+               try {
+                       String[] arr = statusLine.split(" ");
+                       int code = Integer.parseInt(arr[1]);
+                       for (HttpStatus status : values()) {
+                               if (status.getCode() == code)
+                                       return status;
+                       }
+               } catch (Exception e) {
+                       throw new IllegalArgumentException("Invalid status line: " + statusLine, e);
+               }
+               throw new IllegalArgumentException("Unkown status code: " + statusLine);
+       }
+
+       @Override
+       public String toString() {
+               return code + " " + reasonPhrase;
+       }
+
+}
index b7445633b4bc8511ce469571293e4b7e3676b53d..a4c482663e73c86d19f236825317b379fee2e05c 100644 (file)
@@ -1,8 +1,8 @@
 package org.argeo.cms.internal.auth;
 
-import static org.argeo.util.naming.LdapAttrs.cn;
-import static org.argeo.util.naming.LdapAttrs.description;
-import static org.argeo.util.naming.LdapAttrs.owner;
+import static org.argeo.api.acr.ldap.LdapAttrs.cn;
+import static org.argeo.api.acr.ldap.LdapAttrs.description;
+import static org.argeo.api.acr.ldap.LdapAttrs.owner;
 
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
@@ -25,22 +25,22 @@ import javax.naming.ldap.LdapName;
 import javax.security.auth.Subject;
 
 import org.argeo.api.acr.NamespaceUtils;
+import org.argeo.api.acr.ldap.LdapAttrs;
+import org.argeo.api.acr.ldap.NamingUtils;
 import org.argeo.api.cms.CmsConstants;
 import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.directory.HierarchyUnit;
+import org.argeo.api.cms.transaction.WorkTransaction;
 import org.argeo.cms.CmsUserManager;
 import org.argeo.cms.auth.CurrentUser;
 import org.argeo.cms.auth.SystemRole;
 import org.argeo.cms.auth.UserAdminUtils;
-import org.argeo.osgi.useradmin.AggregatingUserAdmin;
-import org.argeo.osgi.useradmin.TokenUtils;
-import org.argeo.osgi.useradmin.UserDirectory;
-import org.argeo.util.directory.DirectoryConf;
-import org.argeo.util.directory.HierarchyUnit;
-import org.argeo.util.directory.ldap.LdapEntry;
-import org.argeo.util.directory.ldap.SharedSecret;
-import org.argeo.util.naming.LdapAttrs;
-import org.argeo.util.naming.NamingUtils;
-import org.argeo.util.transaction.WorkTransaction;
+import org.argeo.cms.directory.ldap.LdapEntry;
+import org.argeo.cms.directory.ldap.SharedSecret;
+import org.argeo.cms.osgi.useradmin.AggregatingUserAdmin;
+import org.argeo.cms.osgi.useradmin.TokenUtils;
+import org.argeo.cms.osgi.useradmin.UserDirectory;
+import org.argeo.cms.runtime.DirectoryConf;
 import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.service.useradmin.Authorization;
 import org.osgi.service.useradmin.Group;
index 73f474637750a2f61aa825cddbdc0d12861df99e..7472908cd565656d09692d23370d2c8a51f2629d 100644 (file)
@@ -8,7 +8,7 @@ import java.util.Set;
 
 import javax.xml.namespace.QName;
 
-import org.argeo.api.acr.CrName;
+import org.argeo.api.acr.ArgeoNamespace;
 import org.argeo.api.acr.NamespaceUtils;
 import org.argeo.cms.auth.RoleNameUtils;
 import org.osgi.service.useradmin.Authorization;
@@ -35,7 +35,7 @@ public final class ImpliedByPrincipal implements Principal {
                this.name = name;
                String cn = RoleNameUtils.getLastRdnValue(name);
                roleName = NamespaceUtils.parsePrefixedName(getNamespaceContext(), cn);
-               if (roleName.getNamespaceURI().equals(CrName.ROLE_NAMESPACE_URI)) {
+               if (roleName.getNamespaceURI().equals(ArgeoNamespace.ROLE_NAMESPACE_URI)) {
                        systemRole = true;
                }
                context = RoleNameUtils.getContext(name);
index 3358ed82574eb53562c955f6a9111ebf82c74aff..85f045bae660b6783d2aa4448e7fdce59af327e1 100644 (file)
@@ -5,7 +5,7 @@ import java.util.Enumeration;
 
 import org.argeo.api.cms.CmsConstants;
 import org.argeo.api.cms.CmsLog;
-import org.argeo.util.directory.DirectoryConf;
+import org.argeo.cms.runtime.DirectoryConf;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.Constants;
 import org.osgi.framework.ServiceReference;
index c36f410e2ac6fa115292396e4db3e69b0066d8d2..c80933a559753203cac8e50ebc95b98afd79fd42 100644 (file)
@@ -25,9 +25,9 @@ import org.argeo.cms.dav.DavDepth;
 import org.argeo.cms.dav.DavHttpHandler;
 import org.argeo.cms.dav.DavPropfind;
 import org.argeo.cms.dav.DavResponse;
+import org.argeo.cms.http.HttpStatus;
 import org.argeo.cms.internal.http.RemoteAuthHttpExchange;
-import org.argeo.util.StreamUtils;
-import org.argeo.util.http.HttpStatus;
+import org.argeo.cms.util.StreamUtils;
 
 import com.sun.net.httpserver.HttpExchange;
 
index 6921de77abc4b07954907ebde4d2d7f679a2dc31..5c3838a0a3134a4a6e24e202ecd20e0c992e8711 100644 (file)
@@ -38,7 +38,7 @@ import org.argeo.api.cms.CmsState;
 import org.argeo.api.uuid.UuidFactory;
 import org.argeo.cms.CmsDeployProperty;
 import org.argeo.cms.auth.ident.IdentClient;
-import org.argeo.util.FsUtils;
+import org.argeo.cms.util.FsUtils;
 
 /**
  * Implementation of a {@link CmsState}, initialising the required services.
index 7f4314b996bc9a0c9d7c5f8042c10445fbaf81e2..6aa490a69ae144f9ec697a0e6c2f39b0ce482762 100644 (file)
@@ -29,14 +29,14 @@ import org.argeo.api.cms.CmsAuth;
 import org.argeo.api.cms.CmsConstants;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.api.cms.CmsState;
+import org.argeo.api.cms.transaction.WorkControl;
+import org.argeo.api.cms.transaction.WorkTransaction;
 import org.argeo.cms.CmsDeployProperty;
-import org.argeo.osgi.useradmin.AggregatingUserAdmin;
-import org.argeo.osgi.useradmin.DirectoryUserAdmin;
-import org.argeo.osgi.useradmin.UserDirectory;
-import org.argeo.util.directory.DirectoryConf;
-import org.argeo.util.naming.dns.DnsBrowser;
-import org.argeo.util.transaction.WorkControl;
-import org.argeo.util.transaction.WorkTransaction;
+import org.argeo.cms.dns.DnsBrowser;
+import org.argeo.cms.osgi.useradmin.AggregatingUserAdmin;
+import org.argeo.cms.osgi.useradmin.DirectoryUserAdmin;
+import org.argeo.cms.osgi.useradmin.UserDirectory;
+import org.argeo.cms.runtime.DirectoryConf;
 import org.ietf.jgss.GSSCredential;
 import org.ietf.jgss.GSSException;
 import org.ietf.jgss.GSSManager;
index 3443d73a67408f0f60a50c7bd2c0b6a87aa18f93..f60d3352e98c61c921b260c7be244183b91c085b 100644 (file)
@@ -17,7 +17,7 @@ import java.util.TreeSet;
 import java.util.stream.Collectors;
 
 import org.argeo.api.cms.ux.CmsTheme;
-import org.argeo.util.StreamUtils;
+import org.argeo.cms.util.StreamUtils;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/FilterRequirement.java b/org.argeo.cms/src/org/argeo/cms/osgi/FilterRequirement.java
new file mode 100644 (file)
index 0000000..5582c34
--- /dev/null
@@ -0,0 +1,42 @@
+package org.argeo.cms.osgi;
+
+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<>();
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingAuthorization.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingAuthorization.java
new file mode 100644 (file)
index 0000000..5013175
--- /dev/null
@@ -0,0 +1,77 @@
+package org.argeo.cms.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;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingUserAdmin.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AggregatingUserAdmin.java
new file mode 100644 (file)
index 0000000..2d438cb
--- /dev/null
@@ -0,0 +1,328 @@
+package org.argeo.cms.osgi.useradmin;
+
+import static org.argeo.cms.osgi.useradmin.DirectoryUserAdmin.toLdapName;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.cms.runtime.DirectoryConf;
+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 DirectoryUserAdmin systemRoles = null;
+       private DirectoryUserAdmin tokens = null;
+       private Map<LdapName, DirectoryUserAdmin> businessRoles = new HashMap<LdapName, DirectoryUserAdmin>();
+
+       // TODO rather use an empty constructor and an init method
+       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 IllegalStateException("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;
+       }
+
+       /** Builds an authorisation by scanning all referentials. */
+       @Override
+       public Authorization getAuthorization(User user) {
+               if (user == null) {// anonymous
+                       return systemRoles.getAuthorization(null);
+               }
+               DirectoryUserAdmin userReferentialOfThisUser = findUserAdmin(user.getName());
+               Authorization rawAuthorization = userReferentialOfThisUser.getAuthorization(user);
+               User retrievedUser = (User) userReferentialOfThisUser.getRole(user.getName());
+               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
+               List<String> rawRoles = Arrays.asList(rawAuthorization.getRoles());
+               List<String> allRoles = new ArrayList<>(rawRoles);
+               for (LdapName otherBaseDn : businessRoles.keySet()) {
+                       if (otherBaseDn.equals(userReferentialOfThisUser.getBaseDn()))
+                               continue;
+                       DirectoryUserAdmin otherUserAdmin = userAdminToUse(user, businessRoles.get(otherBaseDn));
+                       if (otherUserAdmin == null)
+                               continue;
+                       for (String roleStr : rawRoles) {
+                               User role = (User) findUserAdmin(roleStr).getRole(roleStr);
+                               Authorization auth = otherUserAdmin.getAuthorization(role);
+                               allRoles.addAll(Arrays.asList(auth.getRoles()));
+                       }
+
+               }
+
+               // integrate system roles
+               final DirectoryUserAdmin userAdminToUse = userAdminToUse(retrievedUser, userReferentialOfThisUser);
+               Objects.requireNonNull(userAdminToUse);
+
+               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,
+                                       allRoles.toArray(new String[allRoles.size()]));
+                       return authorization;
+               } finally {
+                       if (userAdminToUse != null && userAdminToUse.isScoped()) {
+                               userAdminToUse.destroy();
+                       }
+               }
+       }
+
+       /** Decide whether to scope or not */
+       private DirectoryUserAdmin userAdminToUse(User user, DirectoryUserAdmin userAdmin) {
+               if (userAdmin.isAuthenticated())
+                       return userAdmin;
+               if (user instanceof DirectoryUser) {
+                       return userAdmin;
+               } else if (user instanceof AuthenticatingUser) {
+                       return userAdmin.scope(user).orElse(null);
+               } else {
+                       throw new IllegalArgumentException("Unsupported user type " + user.getClass());
+               }
+
+       }
+
+       /**
+        * 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(UserDirectory ud) {
+               if (!(ud instanceof DirectoryUserAdmin))
+                       throw new IllegalArgumentException("Only " + DirectoryUserAdmin.class.getName() + " is supported");
+               DirectoryUserAdmin userDirectory = (DirectoryUserAdmin) ud;
+               String basePath = userDirectory.getBase();
+               if (isSystemRolesBaseDn(basePath)) {
+                       this.systemRoles = userDirectory;
+                       systemRoles.setExternalRoles(this);
+               } else if (isTokensBaseDn(basePath)) {
+                       this.tokens = userDirectory;
+                       tokens.setExternalRoles(this);
+               } else {
+                       LdapName baseDn = toLdapName(basePath);
+                       if (businessRoles.containsKey(baseDn))
+                               throw new IllegalStateException("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(UserDirectory userDirectory) {
+       }
+
+       private DirectoryUserAdmin findUserAdmin(String name) {
+               try {
+                       return findUserAdmin(new LdapName(name));
+               } catch (InvalidNameException e) {
+                       throw new IllegalArgumentException("Badly formatted name " + name, e);
+               }
+       }
+
+       private DirectoryUserAdmin findUserAdmin(LdapName name) {
+               if (name.startsWith(systemRolesBaseDn))
+                       return systemRoles;
+               if (tokensBaseDn != null && name.startsWith(tokensBaseDn))
+                       return tokens;
+               List<DirectoryUserAdmin> res = new ArrayList<>(1);
+               userDirectories: for (LdapName baseDn : businessRoles.keySet()) {
+                       DirectoryUserAdmin 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 IllegalStateException("Cannot find user admin for " + name);
+               if (res.size() > 1)
+                       throw new IllegalStateException("Multiple user admin found for " + name);
+               return res.get(0);
+       }
+
+       protected boolean isSystemRolesBaseDn(String basePath) {
+               return toLdapName(basePath).equals(systemRolesBaseDn);
+       }
+
+       protected boolean isTokensBaseDn(String basePath) {
+               return tokensBaseDn != null && toLdapName(basePath).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 start() {
+               if (systemRoles == null) {
+                       // TODO do we really need separate system roles?
+                       Hashtable<String, Object> properties = new Hashtable<>();
+                       properties.put(DirectoryConf.baseDn.name(), "ou=roles,ou=system");
+                       systemRoles = new DirectoryUserAdmin(properties);
+               }
+       }
+
+       public void stop() {
+               for (LdapName name : businessRoles.keySet()) {
+                       DirectoryUserAdmin userDirectory = businessRoles.get(name);
+                       destroy(userDirectory);
+               }
+               businessRoles.clear();
+               businessRoles = null;
+               destroy(systemRoles);
+               systemRoles = null;
+       }
+
+       private void destroy(DirectoryUserAdmin userDirectory) {
+               preDestroy(userDirectory);
+               userDirectory.destroy();
+       }
+
+//     protected void removeUserDirectory(UserDirectory userDirectory) {
+//             LdapName baseDn = toLdapName(userDirectory.getContext());
+//             businessRoles.remove(baseDn);
+//             if (userDirectory instanceof DirectoryUserAdmin)
+//                     destroy((DirectoryUserAdmin) userDirectory);
+//     }
+
+       @Deprecated
+       protected void removeUserDirectory(String basePath) {
+               if (isSystemRolesBaseDn(basePath))
+                       throw new IllegalArgumentException("System roles cannot be removed ");
+               LdapName baseDn = toLdapName(basePath);
+               if (!businessRoles.containsKey(baseDn))
+                       throw new IllegalStateException("No user directory registered for " + baseDn);
+               DirectoryUserAdmin userDirectory = businessRoles.remove(baseDn);
+               destroy(userDirectory);
+       }
+
+       /**
+        * Called before each user directory is destroyed, so that additional actions
+        * can be performed.
+        */
+       protected void preDestroy(UserDirectory userDirectory) {
+       }
+
+       public Set<UserDirectory> getUserDirectories() {
+               TreeSet<UserDirectory> res = new TreeSet<>((o1, o2) -> o1.getBase().compareTo(o2.getBase()));
+               res.addAll(businessRoles.values());
+               res.add(systemRoles);
+               return res;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AuthenticatingUser.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/AuthenticatingUser.java
new file mode 100644 (file)
index 0000000..b87dc9b
--- /dev/null
@@ -0,0 +1,83 @@
+package org.argeo.cms.osgi.useradmin;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import javax.naming.ldap.LdapName;
+
+import org.argeo.api.cms.directory.DirectoryDigestUtils;
+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 = DirectoryDigestUtils.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;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryGroup.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryGroup.java
new file mode 100644 (file)
index 0000000..d372c05
--- /dev/null
@@ -0,0 +1,8 @@
+package org.argeo.cms.osgi.useradmin;
+
+import org.osgi.service.useradmin.Group;
+
+/** A group in a user directroy. */
+interface DirectoryGroup extends Group, DirectoryUser {
+//     List<LdapName> getMemberNames();
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUser.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUser.java
new file mode 100644 (file)
index 0000000..8fe0af6
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.cms.osgi.useradmin;
+
+import org.osgi.service.useradmin.User;
+
+/** A user in a user directory. */
+interface DirectoryUser extends User {
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUserAdmin.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/DirectoryUserAdmin.java
new file mode 100644 (file)
index 0000000..59fb05d
--- /dev/null
@@ -0,0 +1,400 @@
+package org.argeo.cms.osgi.useradmin;
+
+import static org.argeo.api.acr.ldap.LdapAttrs.objectClass;
+import static org.argeo.api.acr.ldap.LdapObjs.extensibleObject;
+import static org.argeo.api.acr.ldap.LdapObjs.inetOrgPerson;
+import static org.argeo.api.acr.ldap.LdapObjs.organizationalPerson;
+import static org.argeo.api.acr.ldap.LdapObjs.person;
+import static org.argeo.api.acr.ldap.LdapObjs.top;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+
+import javax.naming.Context;
+import javax.naming.InvalidNameException;
+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 javax.security.auth.Subject;
+import javax.security.auth.kerberos.KerberosTicket;
+
+import org.argeo.api.cms.directory.DirectoryDigestUtils;
+import org.argeo.api.cms.directory.HierarchyUnit;
+import org.argeo.cms.directory.ldap.AbstractLdapDirectory;
+import org.argeo.cms.directory.ldap.LdapDao;
+import org.argeo.cms.directory.ldap.LdapEntry;
+import org.argeo.cms.directory.ldap.LdapEntryWorkingCopy;
+import org.argeo.cms.directory.ldap.LdapNameUtils;
+import org.argeo.cms.directory.ldap.LdifDao;
+import org.argeo.cms.runtime.DirectoryConf;
+import org.argeo.cms.util.CurrentSubject;
+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 class DirectoryUserAdmin extends AbstractLdapDirectory implements UserAdmin, UserDirectory {
+
+       private UserAdmin externalRoles;
+
+       // Transaction
+       public DirectoryUserAdmin(URI uriArg, Dictionary<String, ?> props) {
+               this(uriArg, props, false);
+       }
+
+       public DirectoryUserAdmin(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
+               super(uriArg, props, scoped);
+       }
+
+       public DirectoryUserAdmin(Dictionary<String, ?> props) {
+               this(null, props);
+       }
+
+       /*
+        * ABSTRACT METHODS
+        */
+
+       protected Optional<DirectoryUserAdmin> scope(User user) {
+               if (getDirectoryDao() instanceof LdapDao) {
+                       return scopeLdap(user);
+               } else if (getDirectoryDao() instanceof LdifDao) {
+                       return scopeLdif(user);
+               } else {
+                       throw new IllegalStateException("Unsupported DAO " + getDirectoryDao().getClass());
+               }
+       }
+
+       protected Optional<DirectoryUserAdmin> scopeLdap(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 = cloneConfigProperties();
+               properties.put(Context.SECURITY_PRINCIPAL, username.toString());
+               Object pwdCred = credentials.get(SHARED_STATE_PASSWORD);
+               byte[] pwd = (byte[]) pwdCred;
+               if (pwd != null) {
+                       char[] password = DirectoryDigestUtils.bytesToChars(pwd);
+                       properties.put(Context.SECURITY_CREDENTIALS, new String(password));
+               } else {
+                       properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
+               }
+               DirectoryUserAdmin scopedDirectory = new DirectoryUserAdmin(null, properties, true);
+               scopedDirectory.init();
+               // check connection
+               if (!scopedDirectory.getDirectoryDao().checkConnection())
+                       return Optional.empty();
+               return Optional.of(scopedDirectory);
+       }
+
+       protected Optional<DirectoryUserAdmin> scopeLdif(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 = DirectoryDigestUtils.bytesToChars(pwd);
+                       User directoryUser = (User) getRole(username);
+                       if (!directoryUser.hasCredential(null, password))
+                               throw new IllegalStateException("Invalid credentials");
+               } else {
+                       throw new IllegalStateException("Password is required");
+               }
+               Dictionary<String, Object> properties = cloneConfigProperties();
+               properties.put(DirectoryConf.readOnly.name(), "true");
+               DirectoryUserAdmin scopedUserAdmin = new DirectoryUserAdmin(null, properties, true);
+               // FIXME do it better
+               ((LdifDao) getDirectoryDao()).scope((LdifDao) scopedUserAdmin.getDirectoryDao());
+               // no need to check authentication
+               scopedUserAdmin.init();
+               return Optional.of(scopedUserAdmin);
+       }
+
+       @Override
+       public String getRolePath(Role role) {
+               return nameToRelativePath(LdapNameUtils.toLdapName(role.getName()));
+       }
+
+       @Override
+       public String getRoleSimpleName(Role role) {
+               LdapName dn = LdapNameUtils.toLdapName(role.getName());
+               String name = LdapNameUtils.getLastRdnValue(dn);
+               return name;
+       }
+
+       @Override
+       public Role getRoleByPath(String path) {
+               LdapEntry entry = doGetRole(pathToName(path));
+               if (!(entry instanceof Role)) {
+                       return null;
+//                     throw new IllegalStateException("Path must be a UserAdmin Role.");
+               } else {
+                       return (Role) entry;
+               }
+       }
+
+       protected List<Role> getAllRoles(DirectoryUser user) {
+               List<Role> allRoles = new ArrayList<Role>();
+               if (user != null) {
+                       collectRoles((LdapEntry) user, allRoles);
+                       allRoles.add(user);
+               } else
+                       collectAnonymousRoles(allRoles);
+               return allRoles;
+       }
+
+       private void collectRoles(LdapEntry user, List<Role> allRoles) {
+               List<LdapEntry> allEntries = new ArrayList<>();
+               LdapEntry entry = user;
+               collectGroups(entry, allEntries);
+               for (LdapEntry e : allEntries) {
+                       if (e instanceof Role)
+                               allRoles.add((Role) e);
+               }
+       }
+
+       private void collectAnonymousRoles(List<Role> allRoles) {
+               // TODO gather anonymous roles
+       }
+
+       // USER ADMIN
+       @Override
+       public Role getRole(String name) {
+               return (Role) doGetRole(toLdapName(name));
+       }
+
+       @Override
+       public Role[] getRoles(String filter) throws InvalidSyntaxException {
+               List<? extends Role> res = getRoles(getBaseDn(), filter, true);
+               return res.toArray(new Role[res.size()]);
+       }
+
+       List<DirectoryUser> getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException {
+               LdapEntryWorkingCopy wc = getWorkingCopy();
+//             Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
+               List<LdapEntry> searchRes = getDirectoryDao().doGetEntries(searchBase, filter, deep);
+               List<DirectoryUser> res = new ArrayList<>();
+               for (LdapEntry entry : searchRes)
+                       res.add((DirectoryUser) entry);
+               if (wc != null) {
+                       for (Iterator<DirectoryUser> it = res.iterator(); it.hasNext();) {
+                               DirectoryUser user = (DirectoryUser) it.next();
+                               LdapName dn = LdapNameUtils.toLdapName(user.getName());
+                               if (wc.getDeletedData().containsKey(dn))
+                                       it.remove();
+                       }
+                       Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
+                       for (LdapEntry ldapEntry : wc.getNewData().values()) {
+                               DirectoryUser user = (DirectoryUser) ldapEntry;
+                               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;
+       }
+
+       @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 IllegalArgumentException("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) {
+               String f = "(" + key + "=" + value + ")";
+               List<LdapEntry> users = getDirectoryDao().doGetEntries(getBaseDn(), f, true);
+               for (LdapEntry entry : users)
+                       collectedUsers.add((DirectoryUser) entry);
+       }
+
+       @Override
+       public Authorization getAuthorization(User user) {
+               if (user == null) {// anonymous
+                       return new LdifAuthorization(user, getAllRoles(null));
+               }
+               LdapName userName = toLdapName(user.getName());
+               if (isExternal(userName) && user instanceof LdapEntry) {
+                       List<Role> allRoles = new ArrayList<Role>();
+                       collectRoles((LdapEntry) user, allRoles);
+                       return new LdifAuthorization(user, allRoles);
+               } else {
+
+                       Subject currentSubject = CurrentSubject.current();
+                       if (currentSubject != null //
+                                       && getRealm().isPresent() //
+                                       && !currentSubject.getPrivateCredentials(Authorization.class).isEmpty() //
+                                       && !currentSubject.getPrivateCredentials(KerberosTicket.class).isEmpty()) //
+                       {
+                               // TODO not only Kerberos but also bind scope with kept password ?
+                               Authorization auth = currentSubject.getPrivateCredentials(Authorization.class).iterator().next();
+                               // bind with authenticating user
+                               DirectoryUserAdmin scopedUserAdmin = CurrentSubject.callAs(currentSubject, () -> {
+                                       return scope(new AuthenticatingUser(auth.getName(), new Hashtable<>())).orElseThrow();
+                               });
+                               return getAuthorizationFromScoped(scopedUserAdmin, user);
+                       }
+
+                       if (user instanceof DirectoryUser) {
+                               return new LdifAuthorization(user, getAllRoles((DirectoryUser) user));
+                       } else {
+                               // bind with authenticating user
+                               DirectoryUserAdmin scopedUserAdmin = scope(user).orElseThrow();
+                               return getAuthorizationFromScoped(scopedUserAdmin, user);
+                       }
+               }
+       }
+
+       private Authorization getAuthorizationFromScoped(DirectoryUserAdmin scopedUserAdmin, User user) {
+               try {
+                       DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName());
+                       if (directoryUser == null)
+                               throw new IllegalStateException("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();
+               LdapEntryWorkingCopy wc = getWorkingCopy();
+               LdapName dn = toLdapName(name);
+               if ((getDirectoryDao().entryExists(dn) && !wc.getDeletedData().containsKey(dn))
+                               || wc.getNewData().containsKey(dn))
+                       throw new IllegalArgumentException("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.getDeletedData().containsKey(dn)) {
+                       wc.getDeletedData().remove(dn);
+                       wc.getModifiedData().put(dn, attrs);
+                       return getRole(name);
+               } else {
+                       wc.getModifiedData().put(dn, attrs);
+                       LdapEntry newRole = doCreateRole(dn, type, attrs);
+                       wc.getNewData().put(dn, newRole);
+                       return (Role) newRole;
+               }
+       }
+
+       private LdapEntry doCreateRole(LdapName dn, int type, Attributes attrs) {
+               LdapEntry newRole;
+               BasicAttribute objClass = new BasicAttribute(objectClass.name());
+               if (type == Role.USER) {
+                       String userObjClass = getUserObjectClass();
+                       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 = newUser(dn);
+               } else if (type == Role.GROUP) {
+                       String groupObjClass = getGroupObjectClass();
+                       objClass.add(groupObjClass);
+                       // objClass.add(LdifName.extensibleObject.name());
+                       objClass.add(top.name());
+                       attrs.put(objClass);
+                       newRole = newGroup(dn);
+               } else
+                       throw new IllegalArgumentException("Unsupported type " + type);
+               return newRole;
+       }
+
+       @Override
+       public boolean removeRole(String name) {
+               return removeEntry(LdapNameUtils.toLdapName(name));
+       }
+
+       /*
+        * HIERARCHY
+        */
+       @Override
+       public HierarchyUnit getHierarchyUnit(Role role) {
+               LdapName dn = LdapNameUtils.toLdapName(role.getName());
+               LdapName huDn = LdapNameUtils.getParent(dn);
+               HierarchyUnit hierarchyUnit = getDirectoryDao().doGetHierarchyUnit(huDn);
+               if (hierarchyUnit == null)
+                       throw new IllegalStateException("No hierarchy unit found for " + role);
+               return hierarchyUnit;
+       }
+
+       @Override
+       public Iterable<? extends Role> getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep) {
+               LdapName dn = LdapNameUtils.toLdapName(hierarchyUnit.getBase());
+               try {
+                       return getRoles(dn, filter, deep);
+               } catch (InvalidSyntaxException e) {
+                       throw new IllegalArgumentException("Cannot filter " + filter + " " + dn, e);
+               }
+       }
+
+       /*
+        * ROLES CREATION
+        */
+       protected LdapEntry newUser(LdapName name) {
+               // TODO support devices, applications, etc.
+               return new LdifUser(this, name);
+       }
+
+       protected LdapEntry newGroup(LdapName name) {
+               return new LdifGroup(this, name);
+
+       }
+
+       // GETTERS
+       protected UserAdmin getExternalRoles() {
+               return externalRoles;
+       }
+
+       public void setExternalRoles(UserAdmin externalRoles) {
+               this.externalRoles = externalRoles;
+       }
+
+       /*
+        * STATIC UTILITIES
+        */
+       static LdapName toLdapName(String name) {
+               try {
+                       return new LdapName(name);
+               } catch (InvalidNameException e) {
+                       throw new IllegalArgumentException(name + " is not an LDAP name", e);
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifAuthorization.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifAuthorization.java
new file mode 100644 (file)
index 0000000..3bedeab
--- /dev/null
@@ -0,0 +1,85 @@
+package org.argeo.cms.osgi.useradmin;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.List;
+
+import org.argeo.api.acr.ldap.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.name());
+               if (displayName == null)
+                       displayName = props.get(LdapAttrs.cn.name());
+               if (displayName == null)
+                       displayName = props.get(LdapAttrs.uid.name());
+               if (displayName == null)
+                       displayName = user.getName();
+               if (displayName == null)
+                       throw new IllegalStateException("Cannot set display name for " + user);
+               return displayName.toString();
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifGroup.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifGroup.java
new file mode 100644 (file)
index 0000000..882f34a
--- /dev/null
@@ -0,0 +1,127 @@
+package org.argeo.cms.osgi.useradmin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.naming.InvalidNameException;
+import javax.naming.directory.Attribute;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.cms.directory.ldap.AbstractLdapDirectory;
+import org.osgi.service.useradmin.Role;
+
+/** Directory group implementation */
+class LdifGroup extends LdifUser implements DirectoryGroup {
+       private final String memberAttributeId;
+
+       LdifGroup(AbstractLdapDirectory userAdmin, LdapName dn) {
+               super(userAdmin, dn);
+               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 : getReferences(memberAttributeId)) {
+                       Role role = findRole(ldapName);
+                       if (role == null) {
+                               throw new IllegalStateException("Role " + ldapName + " not found.");
+                       }
+                       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 (NamingException e) {
+//                     throw new IllegalStateException("Cannot get members", e);
+//             }
+//     }
+
+       @Override
+       public Role[] getRequiredMembers() {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public int getType() {
+               return GROUP;
+       }
+
+       protected DirectoryUserAdmin getUserAdmin() {
+               return (DirectoryUserAdmin) getDirectory();
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifUser.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/LdifUser.java
new file mode 100644 (file)
index 0000000..2341ec4
--- /dev/null
@@ -0,0 +1,24 @@
+package org.argeo.cms.osgi.useradmin;
+
+import javax.naming.ldap.LdapName;
+
+import org.argeo.cms.directory.ldap.AbstractLdapDirectory;
+import org.argeo.cms.directory.ldap.DefaultLdapEntry;
+
+/** Directory user implementation */
+class LdifUser extends DefaultLdapEntry implements DirectoryUser {
+       LdifUser(AbstractLdapDirectory userAdmin, LdapName dn) {
+               super(userAdmin, dn);
+       }
+
+       @Override
+       public String getName() {
+               return getDn().toString();
+       }
+
+       @Override
+       public int getType() {
+               return USER;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserDirectory.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserDirectory.java
new file mode 100644 (file)
index 0000000..950a401
--- /dev/null
@@ -0,0 +1,111 @@
+package org.argeo.cms.osgi.useradmin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.api.acr.ldap.LdapAttrs;
+import org.argeo.api.cms.directory.HierarchyUnit;
+import org.argeo.cms.directory.ldap.AbstractLdapDirectory;
+import org.argeo.cms.directory.ldap.AbstractLdapDirectoryDao;
+import org.argeo.cms.directory.ldap.LdapEntry;
+import org.argeo.cms.directory.ldap.LdapEntryWorkingCopy;
+
+/** Pseudo user directory to be used when logging in as OS user. */
+public class OsUserDirectory extends AbstractLdapDirectoryDao {
+       private final String osUsername = System.getProperty("user.name");
+       private final LdapName osUserDn;
+       private final LdapEntry osUser;
+
+       public OsUserDirectory(AbstractLdapDirectory directory) {
+               super(directory);
+               try {
+                       osUserDn = new LdapName(LdapAttrs.uid.name() + "=" + osUsername + "," + directory.getUserBaseRdn() + ","
+                                       + directory.getBaseDn());
+//                     Attributes attributes = new BasicAttributes();
+//                     attributes.put(LdapAttrs.uid.name(), osUsername);
+                       osUser = newUser(osUserDn);
+               } catch (NamingException e) {
+                       throw new IllegalStateException("Cannot create system user", e);
+               }
+       }
+
+       @Override
+       public List<LdapName> getDirectGroups(LdapName dn) {
+               return new ArrayList<>();
+       }
+
+       @Override
+       public boolean entryExists(LdapName dn) {
+               return osUserDn.equals(dn);
+       }
+
+       @Override
+       public boolean checkConnection() {
+               return true;
+       }
+
+       @Override
+       public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException {
+               if (osUserDn.equals(key))
+                       return osUser;
+               else
+                       throw new NameNotFoundException("Not an OS role");
+       }
+
+       @Override
+       public List<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
+               List<LdapEntry> res = new ArrayList<>();
+//             if (f == null || f.match(osUser.getProperties()))
+               res.add(osUser);
+               return res;
+       }
+
+       @Override
+       public HierarchyUnit doGetHierarchyUnit(LdapName dn) {
+               return null;
+       }
+
+       @Override
+       public Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
+               return new ArrayList<>();
+       }
+
+       public void prepare(LdapEntryWorkingCopy wc) {
+
+       }
+
+       public void commit(LdapEntryWorkingCopy wc) {
+
+       }
+
+       public void rollback(LdapEntryWorkingCopy wc) {
+
+       }
+
+       @Override
+       public void init() {
+               // TODO Auto-generated method stub
+
+       }
+
+       @Override
+       public void destroy() {
+               // TODO Auto-generated method stub
+
+       }
+
+       @Override
+       public Attributes doGetAttributes(LdapName name) {
+               try {
+                       return doGetEntry(name).getAttributes();
+               } catch (NameNotFoundException e) {
+                       throw new IllegalStateException(name + " doe not exist in " + getDirectory().getBaseDn(), e);
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserUtils.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/OsUserUtils.java
new file mode 100644 (file)
index 0000000..f718780
--- /dev/null
@@ -0,0 +1,54 @@
+package org.argeo.cms.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;
+
+/** Log in based on JDK-provided OS integration. */
+public class OsUserUtils {
+       private final static String LOGIN_CONTEXT_USER_NIX = "USER_NIX";
+       private final 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() {
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/TokenUtils.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/TokenUtils.java
new file mode 100644 (file)
index 0000000..eb94298
--- /dev/null
@@ -0,0 +1,87 @@
+package org.argeo.cms.osgi.useradmin;
+
+import static org.argeo.api.acr.ldap.LdapAttrs.description;
+import static org.argeo.api.acr.ldap.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.api.acr.ldap.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 IllegalArgumentException("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();
+//     }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/UserDirectory.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/UserDirectory.java
new file mode 100644 (file)
index 0000000..463316b
--- /dev/null
@@ -0,0 +1,19 @@
+package org.argeo.cms.osgi.useradmin;
+
+import org.argeo.api.cms.directory.Directory;
+import org.argeo.api.cms.directory.HierarchyUnit;
+import org.osgi.service.useradmin.Role;
+
+/** Information about a user directory. */
+public interface UserDirectory extends Directory {
+
+       HierarchyUnit getHierarchyUnit(Role role);
+
+       Iterable<? extends Role> getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep);
+
+       String getRolePath(Role role);
+
+       String getRoleSimpleName(Role role);
+
+       Role getRoleByPath(String path);
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/jaas-os.cfg b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/jaas-os.cfg
new file mode 100644 (file)
index 0000000..da04505
--- /dev/null
@@ -0,0 +1,8 @@
+USER_NIX {
+    com.sun.security.auth.module.UnixLoginModule requisite; 
+};
+
+USER_NT {
+    com.sun.security.auth.module.NTLoginModule requisite; 
+};
+
diff --git a/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/package-info.java b/org.argeo.cms/src/org/argeo/cms/osgi/useradmin/package-info.java
new file mode 100644 (file)
index 0000000..766c59b
--- /dev/null
@@ -0,0 +1,2 @@
+/** LDAP and LDIF based OSGi useradmin implementation. */
+package org.argeo.cms.osgi.useradmin;
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/runtime/DirectoryConf.java b/org.argeo.cms/src/org/argeo/cms/runtime/DirectoryConf.java
new file mode 100644 (file)
index 0000000..a4e44cc
--- /dev/null
@@ -0,0 +1,247 @@
+package org.argeo.cms.runtime;
+
+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 org.argeo.api.acr.ldap.NamingUtils;
+import org.argeo.api.cms.directory.DirectoryDigestUtils;
+import org.argeo.cms.directory.ldap.IpaUtils;
+
+/** Properties used to configure user admins. */
+public enum DirectoryConf {
+       /** Base DN (cannot be configured externally) */
+       baseDn(null),
+
+       /** URI of the underlying resource (cannot be configured externally) */
+       uri(null),
+
+       /** User objectClass */
+       userObjectClass("inetOrgPerson"),
+
+       /** Relative base DN for users */
+       userBase("ou=People"),
+
+       /** Groups objectClass */
+       groupObjectClass("groupOfNames"),
+
+       /** Relative base DN for users */
+       groupBase("ou=Groups"),
+
+       /** Relative base DN for users */
+       systemRoleBase("ou=Roles"),
+
+       /** Read-only source */
+       readOnly(null),
+
+       /** Disabled source */
+       disabled(null),
+
+       /** Authentication realm */
+       realm(null),
+
+       /** Override all passwords with this value (typically for testing purposes) */
+       forcedPassword(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";
+
+       private final static String SECURITY_PRINCIPAL = "java.naming.security.principal";
+       private final static String SECURITY_CREDENTIALS = "java.naming.security.credentials";
+
+       /** The default value. */
+       private Object def;
+
+       DirectoryConf(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 DirectoryConf local(String property) {
+               return DirectoryConf.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 (DirectoryConf key : DirectoryConf.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 IllegalArgumentException("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 IllegalArgumentException("Unsupported scheme " + scheme);
+                       Map<String, List<String>> query = NamingUtils.queryToMap(u);
+                       for (String key : query.keySet()) {
+                               DirectoryConf ldapProp = DirectoryConf.valueOf(key);
+                               List<String> values = query.get(key);
+                               if (values.size() == 1) {
+                                       res.put(ldapProp.name(), values.get(0));
+                               } else {
+                                       throw new IllegalArgumentException("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(SECURITY_PRINCIPAL, principal);
+                       if (credentials != null)
+                               res.put(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 (URISyntaxException e) {
+                       throw new IllegalArgumentException("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 IllegalStateException("No baseDn in " + properties);
+               return DirectoryDigestUtils.sha1str(bDn);
+       }
+}
index e473d27994ff8f4e467461166b3b6523c7043fb5..0034e3f7b1a0ef60930a9316d3257931f32d1afe 100644 (file)
@@ -9,6 +9,12 @@ import org.argeo.api.acr.spi.ProvidedRepository;
 import org.argeo.api.cms.CmsContext;
 import org.argeo.api.cms.CmsDeployment;
 import org.argeo.api.cms.CmsState;
+import org.argeo.api.cms.transaction.SimpleTransactionManager;
+import org.argeo.api.cms.transaction.WorkControl;
+import org.argeo.api.cms.transaction.WorkTransaction;
+import org.argeo.api.register.Component;
+import org.argeo.api.register.ComponentRegister;
+import org.argeo.api.register.SimpleRegister;
 import org.argeo.api.uuid.UuidFactory;
 import org.argeo.cms.CmsUserManager;
 import org.argeo.cms.acr.CmsUuidFactory;
@@ -18,12 +24,6 @@ import org.argeo.cms.internal.runtime.CmsDeploymentImpl;
 import org.argeo.cms.internal.runtime.CmsStateImpl;
 import org.argeo.cms.internal.runtime.CmsUserAdmin;
 import org.argeo.cms.internal.runtime.DeployedContentRepository;
-import org.argeo.util.register.Component;
-import org.argeo.util.register.ComponentRegister;
-import org.argeo.util.register.SimpleRegister;
-import org.argeo.util.transaction.SimpleTransactionManager;
-import org.argeo.util.transaction.WorkControl;
-import org.argeo.util.transaction.WorkTransaction;
 import org.osgi.service.useradmin.UserAdmin;
 
 /**
diff --git a/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java b/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java
deleted file mode 100644 (file)
index 3de2e14..0000000
+++ /dev/null
@@ -1,287 +0,0 @@
-package org.argeo.cms.security;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.CharArrayWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.Writer;
-import java.security.Provider;
-import java.security.Security;
-import java.util.Arrays;
-import java.util.Iterator;
-
-import javax.crypto.SecretKey;
-import javax.security.auth.Subject;
-import javax.security.auth.callback.Callback;
-import javax.security.auth.callback.CallbackHandler;
-import javax.security.auth.callback.PasswordCallback;
-import javax.security.auth.callback.TextOutputCallback;
-import javax.security.auth.callback.UnsupportedCallbackException;
-import javax.security.auth.login.LoginContext;
-import javax.security.auth.login.LoginException;
-
-import org.argeo.api.cms.CmsAuth;
-import org.argeo.util.CurrentSubject;
-import org.argeo.util.StreamUtils;
-
-/** username / password based keyring. TODO internationalize */
-public abstract class AbstractKeyring implements Keyring, CryptoKeyring {
-       // public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING";
-
-       // private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT;
-       private CallbackHandler defaultCallbackHandler;
-
-       private String charset = "UTF-8";
-
-       /**
-        * Default provider is bouncy castle, in order to have consistent behaviour
-        * across implementations
-        */
-       private String securityProviderName = "BC";
-
-       /**
-        * Whether the keyring has already been created in the past with a master
-        * password
-        */
-       protected abstract Boolean isSetup();
-
-       /**
-        * Setup the keyring persistently, {@link #isSetup()} must return true
-        * afterwards
-        */
-       protected abstract void setup(char[] password);
-
-       /** Populates the key spec callback */
-       protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback);
-
-       protected abstract void encrypt(String path, InputStream unencrypted);
-
-       protected abstract InputStream decrypt(String path);
-
-       /** Triggers lazy initialization */
-       protected SecretKey getSecretKey(char[] password) {
-               Subject subject = CurrentSubject.current();
-               if (subject == null)
-                       throw new IllegalStateException("Current subject cannot be null");
-               // we assume only one secrete key is available
-               Iterator<SecretKey> iterator = subject.getPrivateCredentials(SecretKey.class).iterator();
-               if (!iterator.hasNext() || password != null) {// not initialized
-                       CallbackHandler callbackHandler = password == null ? new KeyringCallbackHandler()
-                                       : new PasswordProvidedCallBackHandler(password);
-                       ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
-                       Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
-                       try {
-                               LoginContext loginContext = new LoginContext(CmsAuth.LOGIN_CONTEXT_KEYRING, subject, callbackHandler);
-                               loginContext.login();
-                               // FIXME will login even if password is wrong
-                               iterator = subject.getPrivateCredentials(SecretKey.class).iterator();
-                               return iterator.next();
-                       } catch (LoginException e) {
-                               throw new IllegalStateException("Keyring login failed", e);
-                       } finally {
-                               Thread.currentThread().setContextClassLoader(currentContextClassLoader);
-                       }
-
-               } else {
-                       SecretKey secretKey = iterator.next();
-                       if (iterator.hasNext())
-                               throw new IllegalStateException("More than one secret key in private credentials");
-                       return secretKey;
-               }
-       }
-
-       public InputStream getAsStream(String path) {
-               return decrypt(path);
-       }
-
-       public void set(String path, InputStream in) {
-               encrypt(path, in);
-       }
-
-       public char[] getAsChars(String path) {
-               // InputStream in = getAsStream(path);
-               // CharArrayWriter writer = null;
-               // Reader reader = null;
-               try (InputStream in = getAsStream(path);
-                               CharArrayWriter writer = new CharArrayWriter();
-                               Reader reader = new InputStreamReader(in, charset);) {
-                       StreamUtils.copy(reader, writer);
-                       return writer.toCharArray();
-               } catch (IOException e) {
-                       throw new IllegalStateException("Cannot decrypt to char array", e);
-               } finally {
-                       // IOUtils.closeQuietly(reader);
-                       // IOUtils.closeQuietly(in);
-                       // IOUtils.closeQuietly(writer);
-               }
-       }
-
-       public void set(String path, char[] arr) {
-               // ByteArrayOutputStream out = new ByteArrayOutputStream();
-               // ByteArrayInputStream in = null;
-               // Writer writer = null;
-               try (ByteArrayOutputStream out = new ByteArrayOutputStream();
-                               Writer writer = new OutputStreamWriter(out, charset);) {
-                       // writer = new OutputStreamWriter(out, charset);
-                       writer.write(arr);
-                       writer.flush();
-                       // in = new ByteArrayInputStream(out.toByteArray());
-                       try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());) {
-                               set(path, in);
-                       }
-               } catch (IOException e) {
-                       throw new IllegalStateException("Cannot encrypt to char array", e);
-               } finally {
-                       // IOUtils.closeQuietly(writer);
-                       // IOUtils.closeQuietly(out);
-                       // IOUtils.closeQuietly(in);
-               }
-       }
-
-       public void unlock(char[] password) {
-               if (!isSetup())
-                       setup(password);
-               SecretKey secretKey = getSecretKey(password);
-               if (secretKey == null)
-                       throw new IllegalStateException("Could not unlock keyring");
-       }
-
-       protected Provider getSecurityProvider() {
-               return Security.getProvider(securityProviderName);
-       }
-
-       public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) {
-               this.defaultCallbackHandler = defaultCallbackHandler;
-       }
-
-       public void setCharset(String charset) {
-               this.charset = charset;
-       }
-
-       public void setSecurityProviderName(String securityProviderName) {
-               this.securityProviderName = securityProviderName;
-       }
-
-       // @Deprecated
-       // protected static byte[] hash(char[] password, byte[] salt, Integer
-       // iterationCount) {
-       // ByteArrayOutputStream out = null;
-       // OutputStreamWriter writer = null;
-       // try {
-       // out = new ByteArrayOutputStream();
-       // writer = new OutputStreamWriter(out, "UTF-8");
-       // writer.write(password);
-       // MessageDigest pwDigest = MessageDigest.getInstance("SHA-256");
-       // pwDigest.reset();
-       // pwDigest.update(salt);
-       // byte[] btPass = pwDigest.digest(out.toByteArray());
-       // for (int i = 0; i < iterationCount; i++) {
-       // pwDigest.reset();
-       // btPass = pwDigest.digest(btPass);
-       // }
-       // return btPass;
-       // } catch (Exception e) {
-       // throw new CmsException("Cannot hash", e);
-       // } finally {
-       // IOUtils.closeQuietly(out);
-       // IOUtils.closeQuietly(writer);
-       // }
-       //
-       // }
-
-       /**
-        * Convenience method using the underlying callback to ask for a password
-        * (typically used when the password is not saved in the keyring)
-        */
-       protected char[] ask() {
-               PasswordCallback passwordCb = new PasswordCallback("Password", false);
-               Callback[] dialogCbs = new Callback[] { passwordCb };
-               try {
-                       defaultCallbackHandler.handle(dialogCbs);
-                       char[] password = passwordCb.getPassword();
-                       return password;
-               } catch (Exception e) {
-                       throw new IllegalStateException("Cannot ask for a password", e);
-               }
-
-       }
-
-       class KeyringCallbackHandler implements CallbackHandler {
-               public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
-                       // checks
-                       if (callbacks.length != 2)
-                               throw new IllegalArgumentException(
-                                               "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
-                       if (!(callbacks[0] instanceof PasswordCallback))
-                               throw new UnsupportedCallbackException(callbacks[0]);
-                       if (!(callbacks[1] instanceof PBEKeySpecCallback))
-                               throw new UnsupportedCallbackException(callbacks[0]);
-
-                       PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
-                       PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
-
-                       if (isSetup()) {
-                               Callback[] dialogCbs = new Callback[] { passwordCb };
-                               defaultCallbackHandler.handle(dialogCbs);
-                       } else {// setup keyring
-                               TextOutputCallback textCb1 = new TextOutputCallback(TextOutputCallback.INFORMATION,
-                                               "Enter a master password which will protect your private data");
-                               TextOutputCallback textCb2 = new TextOutputCallback(TextOutputCallback.INFORMATION,
-                                               "(for example your credentials to third-party services)");
-                               TextOutputCallback textCb3 = new TextOutputCallback(TextOutputCallback.INFORMATION,
-                                               "Don't forget this password since the data cannot be read without it");
-                               PasswordCallback confirmPasswordCb = new PasswordCallback("Confirm password", false);
-                               // first try
-                               Callback[] dialogCbs = new Callback[] { textCb1, textCb2, textCb3, passwordCb, confirmPasswordCb };
-                               defaultCallbackHandler.handle(dialogCbs);
-
-                               // if passwords different, retry (except if cancelled)
-                               while (passwordCb.getPassword() != null
-                                               && !Arrays.equals(passwordCb.getPassword(), confirmPasswordCb.getPassword())) {
-                                       TextOutputCallback textCb = new TextOutputCallback(TextOutputCallback.ERROR,
-                                                       "The passwords do not match");
-                                       dialogCbs = new Callback[] { textCb, passwordCb, confirmPasswordCb };
-                                       defaultCallbackHandler.handle(dialogCbs);
-                               }
-
-                               if (passwordCb.getPassword() != null) {// not cancelled
-                                       setup(passwordCb.getPassword());
-                               }
-                       }
-
-                       if (passwordCb.getPassword() != null)
-                               handleKeySpecCallback(pbeCb);
-               }
-
-       }
-
-       class PasswordProvidedCallBackHandler implements CallbackHandler {
-               private final char[] password;
-
-               public PasswordProvidedCallBackHandler(char[] password) {
-                       this.password = password;
-               }
-
-               @Override
-               public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
-                       // checks
-                       if (callbacks.length != 2)
-                               throw new IllegalArgumentException(
-                                               "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
-                       if (!(callbacks[0] instanceof PasswordCallback))
-                               throw new UnsupportedCallbackException(callbacks[0]);
-                       if (!(callbacks[1] instanceof PBEKeySpecCallback))
-                               throw new UnsupportedCallbackException(callbacks[0]);
-
-                       PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
-                       passwordCb.setPassword(password);
-                       PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
-                       handleKeySpecCallback(pbeCb);
-               }
-
-       }
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/security/ChecksumFactory.java b/org.argeo.cms/src/org/argeo/cms/security/ChecksumFactory.java
deleted file mode 100644 (file)
index 7344f01..0000000
+++ /dev/null
@@ -1,149 +0,0 @@
-package org.argeo.cms.security;
-
-import java.io.IOException;
-import java.math.BigInteger;
-import java.nio.MappedByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.Base64;
-import java.util.zip.Checksum;
-
-/** Allows to fine tune how files are read. */
-public class ChecksumFactory {
-       private int regionSize = 10 * 1024 * 1024;
-
-       public byte[] digest(Path path, final String algo) {
-               try {
-                       final MessageDigest md = MessageDigest.getInstance(algo);
-                       if (Files.isDirectory(path)) {
-                               long begin = System.currentTimeMillis();
-                               Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
-
-                                       @Override
-                                       public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                                               if (!Files.isDirectory(file)) {
-                                                       byte[] digest = digest(file, algo);
-                                                       md.update(digest);
-                                               }
-                                               return FileVisitResult.CONTINUE;
-                                       }
-
-                               });
-                               byte[] digest = md.digest();
-                               long duration = System.currentTimeMillis() - begin;
-                               System.out.println(printBase64Binary(digest) + " " + path + " (" + duration / 1000 + "s)");
-                               return digest;
-                       } else {
-                               long begin = System.nanoTime();
-                               long length = -1;
-                               try (FileChannel channel = (FileChannel) Files.newByteChannel(path);) {
-                                       length = channel.size();
-                                       long cursor = 0;
-                                       while (cursor < length) {
-                                               long effectiveSize = Math.min(regionSize, length - cursor);
-                                               MappedByteBuffer mb = channel.map(FileChannel.MapMode.READ_ONLY, cursor, effectiveSize);
-                                               // md.update(mb);
-                                               byte[] buffer = new byte[1024];
-                                               while (mb.hasRemaining()) {
-                                                       mb.get(buffer);
-                                                       md.update(buffer);
-                                               }
-
-                                               // sub digest
-                                               // mb.flip();
-                                               // MessageDigest subMd =
-                                               // MessageDigest.getInstance(algo);
-                                               // subMd.update(mb);
-                                               // byte[] subDigest = subMd.digest();
-                                               // System.out.println(" -> " + cursor);
-                                               // System.out.println(IOUtils.encodeHexString(subDigest));
-                                               // System.out.println(new BigInteger(1,
-                                               // subDigest).toString(16));
-                                               // System.out.println(new BigInteger(1, subDigest)
-                                               // .toString(Character.MAX_RADIX));
-                                               // System.out.println(printBase64Binary(subDigest));
-
-                                               cursor = cursor + regionSize;
-                                       }
-                                       byte[] digest = md.digest();
-                                       long duration = System.nanoTime() - begin;
-                                       System.out.println(printBase64Binary(digest) + " " + path.getFileName() + " (" + duration / 1000000
-                                                       + "ms, " + (length / 1024) + "kB, " + (length / (duration / 1000000)) * 1000 / (1024 * 1024)
-                                                       + " MB/s)");
-                                       return digest;
-                               }
-                       }
-               } catch (NoSuchAlgorithmException | IOException e) {
-                       throw new IllegalStateException("Cannot digest " + path, e);
-               }
-       }
-
-       /** Whether the file should be mapped. */
-       protected boolean mapFile(FileChannel fileChannel) throws IOException {
-               long size = fileChannel.size();
-               if (size > (regionSize / 10))
-                       return true;
-               return false;
-       }
-
-       public long checksum(Path path, Checksum crc) {
-               final int bufferSize = 2 * 1024 * 1024;
-               long begin = System.currentTimeMillis();
-               try (FileChannel channel = (FileChannel) Files.newByteChannel(path);) {
-                       byte[] bytes = new byte[bufferSize];
-                       long length = channel.size();
-                       long cursor = 0;
-                       while (cursor < length) {
-                               long effectiveSize = Math.min(regionSize, length - cursor);
-                               MappedByteBuffer mb = channel.map(FileChannel.MapMode.READ_ONLY, cursor, effectiveSize);
-                               int nGet;
-                               while (mb.hasRemaining()) {
-                                       nGet = Math.min(mb.remaining(), bufferSize);
-                                       mb.get(bytes, 0, nGet);
-                                       crc.update(bytes, 0, nGet);
-                               }
-                               cursor = cursor + regionSize;
-                       }
-                       return crc.getValue();
-               } catch (IOException e) {
-                       throw new IllegalStateException("Cannot checksum " + path, e);
-               } finally {
-                       long duration = System.currentTimeMillis() - begin;
-                       System.out.println(duration / 1000 + "s");
-               }
-       }
-
-       public static void main(String... args) {
-               ChecksumFactory cf = new ChecksumFactory();
-               // Path path =
-               // Paths.get("/home/mbaudier/apache-maven-3.2.3-bin.tar.gz");
-               Path path;
-               if (args.length > 0) {
-                       path = Paths.get(args[0]);
-               } else {
-                       path = Paths.get("/home/mbaudier/Downloads/torrents/CentOS-7-x86_64-DVD-1503-01/"
-                                       + "CentOS-7-x86_64-DVD-1503-01.iso");
-               }
-               // long adler = cf.checksum(path, new Adler32());
-               // System.out.format("Adler=%d%n", adler);
-               // long crc = cf.checksum(path, new CRC32());
-               // System.out.format("CRC=%d%n", crc);
-               String algo = "SHA1";
-               byte[] digest = cf.digest(path, algo);
-               System.out.println(algo + " " + printBase64Binary(digest));
-               System.out.println(algo + " " + new BigInteger(1, digest).toString(16));
-               // String sha1 = printBase64Binary(cf.digest(path, "SHA1"));
-               // System.out.format("SHA1=%s%n", sha1);
-       }
-
-       private static String printBase64Binary(byte[] arr) {
-               return Base64.getEncoder().encodeToString(arr);
-       }
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/security/CryptoKeyring.java b/org.argeo.cms/src/org/argeo/cms/security/CryptoKeyring.java
deleted file mode 100644 (file)
index df26c6b..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.argeo.cms.security;
-
-/**
- * Marker interface for an advanced keyring based on cryptography.
- */
-public interface CryptoKeyring extends Keyring {
-       public void changePassword(char[] oldPassword, char[] newPassword);
-
-       public void unlock(char[] password);
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/security/Keyring.java b/org.argeo.cms/src/org/argeo/cms/security/Keyring.java
deleted file mode 100644 (file)
index 53740c6..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.argeo.cms.security;
-
-import java.io.InputStream;
-
-/**
- * Access to private (typically encrypted) data. The keyring is responsible for
- * retrieving the necessary credentials. <b>Experimental. This API may
- * change.</b>
- */
-public interface Keyring {
-       /**
-        * Returns the confidential information as chars. Must ask for it if it is
-        * not stored.
-        */
-       public char[] getAsChars(String path);
-
-       /**
-        * Returns the confidential information as a stream. Must ask for it if it
-        * is not stored.
-        */
-       public InputStream getAsStream(String path);
-
-       public void set(String path, char[] arr);
-
-       public void set(String path, InputStream in);
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/security/PBEKeySpecCallback.java b/org.argeo.cms/src/org/argeo/cms/security/PBEKeySpecCallback.java
deleted file mode 100644 (file)
index 13e8d75..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-package org.argeo.cms.security;
-
-import javax.crypto.spec.PBEKeySpec;
-import javax.security.auth.callback.Callback;
-import javax.security.auth.callback.PasswordCallback;
-
-/**
- * All information required to set up a {@link PBEKeySpec} bar the password
- * itself (use a {@link PasswordCallback})
- */
-public class PBEKeySpecCallback implements Callback {
-       private String secretKeyFactory;
-       private byte[] salt;
-       private Integer iterationCount;
-       /** Can be null for some algorithms */
-       private Integer keyLength;
-       /** Can be null, will trigger secret key encryption if not */
-       private String secretKeyEncryption;
-
-       private String encryptedPasswordHashCipher;
-       private byte[] encryptedPasswordHash;
-
-       public void set(String secretKeyFactory, byte[] salt,
-                       Integer iterationCount, Integer keyLength,
-                       String secretKeyEncryption) {
-               this.secretKeyFactory = secretKeyFactory;
-               this.salt = salt;
-               this.iterationCount = iterationCount;
-               this.keyLength = keyLength;
-               this.secretKeyEncryption = secretKeyEncryption;
-//             this.encryptedPasswordHashCipher = encryptedPasswordHashCipher;
-//             this.encryptedPasswordHash = encryptedPasswordHash;
-       }
-
-       public String getSecretKeyFactory() {
-               return secretKeyFactory;
-       }
-
-       public byte[] getSalt() {
-               return salt;
-       }
-
-       public Integer getIterationCount() {
-               return iterationCount;
-       }
-
-       public Integer getKeyLength() {
-               return keyLength;
-       }
-
-       public String getSecretKeyEncryption() {
-               return secretKeyEncryption;
-       }
-
-       public String getEncryptedPasswordHashCipher() {
-               return encryptedPasswordHashCipher;
-       }
-
-       public byte[] getEncryptedPasswordHash() {
-               return encryptedPasswordHash;
-       }
-
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/security/package-info.java b/org.argeo.cms/src/org/argeo/cms/security/package-info.java
deleted file mode 100644 (file)
index e994054..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/** Argeo CMS reusable security components. */
-package org.argeo.cms.security;
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/ArrayTabularRow.java b/org.argeo.cms/src/org/argeo/cms/tabular/ArrayTabularRow.java
deleted file mode 100644 (file)
index cfd4827..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.argeo.cms.tabular;
-
-import java.util.List;
-
-/** Minimal tabular row wrapping an {@link Object} array */
-public class ArrayTabularRow implements TabularRow {
-       private final Object[] arr;
-
-       public ArrayTabularRow(List<?> objs) {
-               this.arr = objs.toArray();
-       }
-
-       public Object get(Integer col) {
-               return arr[col];
-       }
-
-       public int size() {
-               return arr.length;
-       }
-
-       public Object[] toArray() {
-               return arr;
-       }
-
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/TabularColumn.java b/org.argeo.cms/src/org/argeo/cms/tabular/TabularColumn.java
deleted file mode 100644 (file)
index 7f7ac1e..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.argeo.cms.tabular;
-
-/** The column in a tabular content */
-public class TabularColumn {
-       private String name;
-       /**
-        * JCR types, see
-        * http://www.day.com/maven/javax.jcr/javadocs/jcr-2.0/index.html
-        * ?javax/jcr/PropertyType.html
-        */
-       private Integer type;
-
-       /** column with default type */
-       public TabularColumn(String name) {
-               super();
-               this.name = name;
-       }
-
-       public TabularColumn(String name, Integer type) {
-               super();
-               this.name = name;
-               this.type = type;
-       }
-
-       public String getName() {
-               return name;
-       }
-
-       public void setName(String name) {
-               this.name = name;
-       }
-
-       public Integer getType() {
-               return type;
-       }
-
-       public void setType(Integer type) {
-               this.type = type;
-       }
-
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/TabularContent.java b/org.argeo.cms/src/org/argeo/cms/tabular/TabularContent.java
deleted file mode 100644 (file)
index c6d2ab8..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.argeo.cms.tabular;
-
-import java.util.List;
-
-/**
- * Content organized as a table, possibly with headers. Only JCR types are
- * supported even though there is not direct dependency on JCR.
- */
-public interface TabularContent {
-       /** The headers of this table or <code>null</code> is none available. */
-       public List<TabularColumn> getColumns();
-
-       public TabularRowIterator read();
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/TabularRow.java b/org.argeo.cms/src/org/argeo/cms/tabular/TabularRow.java
deleted file mode 100644 (file)
index 69b9732..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.argeo.cms.tabular;
-
-/** A row of tabular data */
-public interface TabularRow {
-       /** The value at this column index */
-       public Object get(Integer col);
-
-       /** The raw objects (direct references) */
-       public Object[] toArray();
-
-       /** Number of columns */
-       public int size();
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/TabularRowIterator.java b/org.argeo.cms/src/org/argeo/cms/tabular/TabularRowIterator.java
deleted file mode 100644 (file)
index 7ad8719..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.argeo.cms.tabular;
-
-import java.util.Iterator;
-
-/** Navigation of rows */
-public interface TabularRowIterator extends Iterator<TabularRow> {
-       /**
-        * Current row number, has to be incremented by each call to next() ; starts at 0, will
-        * therefore be 1 for the first row returned.
-        */
-       public Long getCurrentRowNumber();
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/TabularWriter.java b/org.argeo.cms/src/org/argeo/cms/tabular/TabularWriter.java
deleted file mode 100644 (file)
index 34fc85b..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.argeo.cms.tabular;
-
-
-/** Write to a tabular content */
-public interface TabularWriter {
-       /** Append a new row of data */
-       public void appendRow(Object[] row);
-
-       /** Finish persisting data and release resources */
-       public void close();
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/package-info.java b/org.argeo.cms/src/org/argeo/cms/tabular/package-info.java
deleted file mode 100644 (file)
index 6cb48d0..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/** Tabular format API. */
-package org.argeo.cms.tabular;
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/util/CompositeString.java b/org.argeo.cms/src/org/argeo/cms/util/CompositeString.java
new file mode 100644 (file)
index 0000000..8ea16f7
--- /dev/null
@@ -0,0 +1,164 @@
+package org.argeo.cms.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.StringTokenizer;
+
+/** A name that can be expressed with various conventions. */
+public class CompositeString {
+       public final static Character UNDERSCORE = Character.valueOf('_');
+       public final static Character SPACE = Character.valueOf(' ');
+       public final static Character DASH = Character.valueOf('-');
+
+       private final String[] parts;
+
+       // optimisation
+       private final int hashCode;
+
+       public CompositeString(String str) {
+               Objects.requireNonNull(str, "String cannot be null");
+               if ("".equals(str.trim()))
+                       throw new IllegalArgumentException("String cannot be empty");
+               if (!str.equals(str.trim()))
+                       throw new IllegalArgumentException("String must be trimmed");
+               this.parts = toParts(str);
+               hashCode = hashCode(this.parts);
+       }
+
+       public String toString(char separator, boolean upperCase) {
+               StringBuilder sb = null;
+               for (String part : parts) {
+                       if (sb == null) {
+                               sb = new StringBuilder();
+                       } else {
+                               sb.append(separator);
+                       }
+                       sb.append(upperCase ? part.toUpperCase() : part);
+               }
+               return sb.toString();
+       }
+
+       public String toStringCaml(boolean firstCharUpperCase) {
+               StringBuilder sb = null;
+               for (String part : parts) {
+                       if (sb == null) {// first
+                               sb = new StringBuilder();
+                               sb.append(firstCharUpperCase ? Character.toUpperCase(part.charAt(0)) : part.charAt(0));
+                       } else {
+                               sb.append(Character.toUpperCase(part.charAt(0)));
+                       }
+
+                       if (part.length() > 1)
+                               sb.append(part.substring(1));
+               }
+               return sb.toString();
+       }
+
+       @Override
+       public int hashCode() {
+               return hashCode;
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj == null || !(obj instanceof CompositeString))
+                       return false;
+
+               CompositeString other = (CompositeString) obj;
+               return Arrays.equals(parts, other.parts);
+       }
+
+       @Override
+       public String toString() {
+               return toString(DASH, false);
+       }
+
+       public static String[] toParts(String str) {
+               Character separator = null;
+               if (str.indexOf(UNDERSCORE) >= 0) {
+                       checkNo(str, SPACE);
+                       checkNo(str, DASH);
+                       separator = UNDERSCORE;
+               } else if (str.indexOf(DASH) >= 0) {
+                       checkNo(str, SPACE);
+                       checkNo(str, UNDERSCORE);
+                       separator = DASH;
+               } else if (str.indexOf(SPACE) >= 0) {
+                       checkNo(str, DASH);
+                       checkNo(str, UNDERSCORE);
+                       separator = SPACE;
+               }
+
+               List<String> res = new ArrayList<>();
+               if (separator != null) {
+                       StringTokenizer st = new StringTokenizer(str, separator.toString());
+                       while (st.hasMoreTokens()) {
+                               res.add(st.nextToken().toLowerCase());
+                       }
+               } else {
+                       // single
+                       String strLowerCase = str.toLowerCase();
+                       if (str.toUpperCase().equals(str) || strLowerCase.equals(str))
+                               return new String[] { strLowerCase };
+
+                       // CAML
+                       StringBuilder current = null;
+                       for (char c : str.toCharArray()) {
+                               if (Character.isUpperCase(c)) {
+                                       if (current != null)
+                                               res.add(current.toString());
+                                       current = new StringBuilder();
+                               }
+                               if (current == null)// first char is lower case
+                                       current = new StringBuilder();
+                               current.append(Character.toLowerCase(c));
+                       }
+                       res.add(current.toString());
+               }
+               return res.toArray(new String[res.size()]);
+       }
+
+       private static void checkNo(String str, Character c) {
+               if (str.indexOf(c) >= 0) {
+                       throw new IllegalArgumentException("Only one kind of sperator is allowed");
+               }
+       }
+
+       private static int hashCode(String[] parts) {
+               int hashCode = 0;
+               for (String part : parts) {
+                       hashCode = hashCode + part.hashCode();
+               }
+               return hashCode;
+       }
+
+       static boolean smokeTests() {
+               CompositeString plainName = new CompositeString("NAME");
+               assert "name".equals(plainName.toString());
+               assert "NAME".equals(plainName.toString(UNDERSCORE, true));
+               assert "name".equals(plainName.toString(UNDERSCORE, false));
+               assert "name".equals(plainName.toStringCaml(false));
+               assert "Name".equals(plainName.toStringCaml(true));
+
+               CompositeString camlName = new CompositeString("myComplexName");
+
+               assert new CompositeString("my-complex-name").equals(camlName);
+               assert new CompositeString("MY_COMPLEX_NAME").equals(camlName);
+               assert new CompositeString("My complex Name").equals(camlName);
+               assert new CompositeString("MyComplexName").equals(camlName);
+
+               assert "my-complex-name".equals(camlName.toString());
+               assert "MY_COMPLEX_NAME".equals(camlName.toString(UNDERSCORE, true));
+               assert "my_complex_name".equals(camlName.toString(UNDERSCORE, false));
+               assert "myComplexName".equals(camlName.toStringCaml(false));
+               assert "MyComplexName".equals(camlName.toStringCaml(true));
+
+               return CompositeString.class.desiredAssertionStatus();
+       }
+
+       public static void main(String[] args) {
+               System.out.println(smokeTests());
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/CsvParser.java b/org.argeo.cms/src/org/argeo/cms/util/CsvParser.java
new file mode 100644 (file)
index 0000000..f22a1e4
--- /dev/null
@@ -0,0 +1,242 @@
+package org.argeo.cms.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;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/CsvParserWithLinesAsMap.java b/org.argeo.cms/src/org/argeo/cms/util/CsvParserWithLinesAsMap.java
new file mode 100644 (file)
index 0000000..0a0382c
--- /dev/null
@@ -0,0 +1,36 @@
+package org.argeo.cms.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);
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/CsvWriter.java b/org.argeo.cms/src/org/argeo/cms/util/CsvWriter.java
new file mode 100644 (file)
index 0000000..915a97f
--- /dev/null
@@ -0,0 +1,156 @@
+package org.argeo.cms.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;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/CurrentSubject.java b/org.argeo.cms/src/org/argeo/cms/util/CurrentSubject.java
new file mode 100644 (file)
index 0000000..6a3dcbc
--- /dev/null
@@ -0,0 +1,65 @@
+package org.argeo.cms.util;
+
+import java.security.AccessController;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionException;
+
+import javax.security.auth.Subject;
+
+/**
+ * Prepare evolution of Java APIs introduced in JDK 18, as these static methods
+ * will be added to {@link Subject}.
+ */
+@SuppressWarnings("removal")
+public class CurrentSubject {
+
+       private final static boolean useThreadLocal = Boolean
+                       .parseBoolean(System.getProperty("jdk.security.auth.subject.useTL"));
+
+       private final static InheritableThreadLocal<Subject> current = new InheritableThreadLocal<>();
+
+       public static Subject current() {
+               if (useThreadLocal) {
+                       return current.get();
+               } else {// legacy
+                       Subject subject = Subject.getSubject(AccessController.getContext());
+                       return subject;
+               }
+       }
+
+       public static <T> T callAs(Subject subject, Callable<T> action) {
+               if (useThreadLocal) {
+                       Subject previous = current();
+                       current.set(subject);
+                       try {
+                               return action.call();
+                       } catch (Exception e) {
+                               throw new CompletionException("Failed to execute action for " + subject, e);
+                       } finally {
+                               current.set(previous);
+                       }
+               } else {// legacy
+                       try {
+                               return Subject.doAs(subject, new PrivilegedExceptionAction<T>() {
+
+                                       @Override
+                                       public T run() throws Exception {
+                                               return action.call();
+                                       }
+
+                               });
+                       } catch (PrivilegedActionException e) {
+                               throw new CompletionException("Failed to execute action for " + subject, e.getCause());
+                       } catch (Exception e) {
+                               throw new CompletionException("Failed to execute action for " + subject, e);
+                       }
+               }
+       }
+
+       /** Singleton. */
+       private CurrentSubject() {
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/DictionaryKeys.java b/org.argeo.cms/src/org/argeo/cms/util/DictionaryKeys.java
new file mode 100644 (file)
index 0000000..a9f6a31
--- /dev/null
@@ -0,0 +1,42 @@
+package org.argeo.cms.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();
+               }
+
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/DigestUtils.java b/org.argeo.cms/src/org/argeo/cms/util/DigestUtils.java
new file mode 100644 (file)
index 0000000..047749f
--- /dev/null
@@ -0,0 +1,202 @@
+package org.argeo.cms.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);
+                       for (byte[] arr : bytes)
+                               digest.update(arr);
+                       byte[] checksum = digest.digest();
+                       return checksum;
+               } catch (NoSuchAlgorithmException e) {
+                       throw new UnsupportedOperationException("SHA1 is not avalaible", e);
+               }
+       }
+
+       public static byte[] digestAsBytes(String algorithm, byte[]... bytes) {
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(algorithm);
+                       for (byte[] arr : bytes)
+                               digest.update(arr);
+                       byte[] checksum = digest.digest();
+                       return checksum;
+               } catch (NoSuchAlgorithmException e) {
+                       throw new UnsupportedOperationException("Cannot digest with algorithm " + algorithm, e);
+               }
+       }
+
+       public static String digest(String algorithm, byte[]... bytes) {
+               return toHexString(digestAsBytes(algorithm, bytes));
+       }
+
+       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 = toHexString(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 = toHexString(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 = digestAsBytes(algorithm, path, bufferSize);
+               return toHexString(digest);
+       }
+
+       public static byte[] digestAsBytes(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();
+
+       /** Converts a byte array to an hex String. */
+       public static String toHexString(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);
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/DirH.java b/org.argeo.cms/src/org/argeo/cms/util/DirH.java
new file mode 100644 (file)
index 0000000..2596c61
--- /dev/null
@@ -0,0 +1,116 @@
+package org.argeo.cms.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.toHexString(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.toHexString(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.digestAsBytes(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();
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/ExceptionsChain.java b/org.argeo.cms/src/org/argeo/cms/util/ExceptionsChain.java
new file mode 100644 (file)
index 0000000..e71cfb3
--- /dev/null
@@ -0,0 +1,90 @@
+package org.argeo.cms.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Serialisable wrapper of a {@link Throwable}. typically to be written as XML
+ * or JSON in a server error response.
+ */
+public class ExceptionsChain {
+       private List<SystemException> exceptions = new ArrayList<>();
+
+       public ExceptionsChain() {
+       }
+
+       public ExceptionsChain(Throwable exception) {
+               writeException(exception);
+       }
+
+       /** recursive */
+       protected void writeException(Throwable exception) {
+               SystemException systemException = new SystemException(exception);
+               exceptions.add(systemException);
+               Throwable cause = exception.getCause();
+               if (cause != null)
+                       writeException(cause);
+       }
+
+       public List<SystemException> getExceptions() {
+               return exceptions;
+       }
+
+       public void setExceptions(List<SystemException> exceptions) {
+               this.exceptions = exceptions;
+       }
+
+       /** An exception in the chain. */
+       public static class SystemException {
+               private String type;
+               private String message;
+               private List<String> stackTrace;
+
+               public SystemException() {
+               }
+
+               public SystemException(Throwable exception) {
+                       this.type = exception.getClass().getName();
+                       this.message = exception.getMessage();
+                       this.stackTrace = new ArrayList<>();
+                       StackTraceElement[] elems = exception.getStackTrace();
+                       for (int i = 0; i < elems.length; i++)
+                               stackTrace.add("at " + elems[i].toString());
+               }
+
+               public String getType() {
+                       return type;
+               }
+
+               public void setType(String type) {
+                       this.type = type;
+               }
+
+               public String getMessage() {
+                       return message;
+               }
+
+               public void setMessage(String message) {
+                       this.message = message;
+               }
+
+               public List<String> getStackTrace() {
+                       return stackTrace;
+               }
+
+               public void setStackTrace(List<String> stackTrace) {
+                       this.stackTrace = stackTrace;
+               }
+
+               @Override
+               public String toString() {
+                       return "System exception: " + type + ", " + message + ", " + stackTrace;
+               }
+
+       }
+
+       @Override
+       public String toString() {
+               return exceptions.toString();
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/FsUtils.java b/org.argeo.cms/src/org/argeo/cms/util/FsUtils.java
new file mode 100644 (file)
index 0000000..26c05b6
--- /dev/null
@@ -0,0 +1,78 @@
+package org.argeo.cms.util;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+
+/** Utilities around the standard Java file abstractions. */
+public class FsUtils {
+
+       /** Deletes this path, recursively if needed. */
+       public static void copyDirectory(Path source, Path target) {
+               if (!Files.exists(source) || !Files.isDirectory(source))
+                       throw new IllegalArgumentException(source + " is not a directory");
+               if (Files.exists(target) && !Files.isDirectory(target))
+                       throw new IllegalArgumentException(target + " is not a directory");
+               try {
+                       Files.createDirectories(target);
+                       Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
+
+                               @Override
+                               public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs) throws IOException {
+                                       Path relativePath = source.relativize(directory);
+                                       Path targetDirectory = target.resolve(relativePath);
+                                       if (!Files.exists(targetDirectory))
+                                               Files.createDirectory(targetDirectory);
+                                       return FileVisitResult.CONTINUE;
+                               }
+
+                               @Override
+                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                       Path relativePath = source.relativize(file);
+                                       Path targetFile = target.resolve(relativePath);
+                                       Files.copy(file, targetFile);
+                                       return FileVisitResult.CONTINUE;
+                               }
+                       });
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot copy " + source + " to " + target, e);
+               }
+
+       }
+
+       /**
+        * Deletes this path, recursively if needed. Does nothing if the path does not
+        * exist.
+        */
+       public static void delete(Path path) {
+               try {
+                       if (!Files.exists(path))
+                               return;
+                       Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
+                               @Override
+                               public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
+                                       if (e != null)
+                                               throw e;
+                                       Files.delete(directory);
+                                       return FileVisitResult.CONTINUE;
+                               }
+
+                               @Override
+                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                       Files.delete(file);
+                                       return FileVisitResult.CONTINUE;
+                               }
+                       });
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot delete " + path, e);
+               }
+       }
+
+       /** Singleton. */
+       private FsUtils() {
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/LangUtils.java b/org.argeo.cms/src/org/argeo/cms/util/LangUtils.java
new file mode 100644 (file)
index 0000000..0e21427
--- /dev/null
@@ -0,0 +1,331 @@
+package org.argeo.cms.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.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+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 Map} with one key-value pair. Key should not be null,
+//      * but if the value is null, it returns an empty {@link Map}.
+//      * 
+//      * @deprecated Use {@link Collections#singletonMap(Object, Object)} instead.
+//      */
+//     @Deprecated
+//     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;
+       }
+
+       /** Converts a {@link Dictionary} to a {@link Map}. */
+       public static Map<String, Object> dictToMap(Dictionary<String, ?> properties) {
+               if (properties == null) {
+                       return null;
+               }
+               Map<String, Object> res = new HashMap<>(properties.size());
+               Enumeration<String> keys = properties.keys();
+               while (keys.hasMoreElements()) {
+                       String key = keys.nextElement();
+                       res.put(key, properties.get(key));
+               }
+               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;
+       }
+
+       /** Size of an {@link Iterable}, optimised if it is a {@link Collection}. */
+       public static int size(Iterable<?> iterable) {
+               if (iterable instanceof Collection)
+                       return ((Collection<?>) iterable).size();
+
+               int size = 0;
+               for (Iterator<?> it = iterable.iterator(); it.hasNext(); size++)
+                       it.next();
+               return size;
+       }
+
+       public static <T> T getAt(Iterable<T> iterable, int index) {
+               if (iterable instanceof List) {
+                       List<T> lst = ((List<T>) iterable);
+                       if (index >= lst.size())
+                               throw new IllegalArgumentException("Index " + index + " is not available (size is " + lst.size() + ")");
+                       return lst.get(index);
+               }
+               int i = 0;
+               for (Iterator<T> it = iterable.iterator(); it.hasNext(); i++) {
+                       if (i == index)
+                               return it.next();
+                       else
+                               it.next();
+               }
+               throw new IllegalArgumentException("Index " + index + " is not available (size is " + i + ")");
+       }
+
+       /*
+        * 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() {
+
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/OS.java b/org.argeo.cms/src/org/argeo/cms/util/OS.java
new file mode 100644 (file)
index 0000000..c63d7a1
--- /dev/null
@@ -0,0 +1,65 @@
+package org.argeo.cms.util;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+/** 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/bash", "-l", "-i" };
+               else
+                       return new String[] { "cmd.exe", "/C" };
+       }
+
+       public static long getJvmPid() {
+               return ProcessHandle.current().pid();
+//             String pidAndHost = ManagementFactory.getRuntimeMXBean().getName();
+//             return Integer.parseInt(pidAndHost.substring(0, pidAndHost.indexOf('@')));
+       }
+
+       /**
+        * Get the runtime directory. It will be the environment variable
+        * XDG_RUNTIME_DIR if it is set, or ~/.cache/argeo if not.
+        */
+       public static Path getRunDir() {
+               Path runDir;
+               String xdgRunDir = System.getenv("XDG_RUNTIME_DIR");
+               if (xdgRunDir != null) {
+                       // TODO support multiple names
+                       runDir = Paths.get(xdgRunDir);
+               } else {
+                       runDir = Paths.get(System.getProperty("user.home"), ".cache/argeo");
+               }
+               return runDir;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/PasswordEncryption.java b/org.argeo.cms/src/org/argeo/cms/util/PasswordEncryption.java
new file mode 100644 (file)
index 0000000..c50f415
--- /dev/null
@@ -0,0 +1,216 @@
+package org.argeo.cms.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;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/ServiceChannel.java b/org.argeo.cms/src/org/argeo/cms/util/ServiceChannel.java
new file mode 100644 (file)
index 0000000..8cdbcad
--- /dev/null
@@ -0,0 +1,78 @@
+package org.argeo.cms.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;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java b/org.argeo.cms/src/org/argeo/cms/util/StreamUtils.java
new file mode 100644 (file)
index 0000000..a589e73
--- /dev/null
@@ -0,0 +1,98 @@
+package org.argeo.cms.util;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.StringJoiner;
+
+/** Stream utilities to be used when Apache Commons IO is not available. */
+public 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 byte[] toByteArray(InputStream in) throws IOException {
+               try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+                       copy(in, out);
+                       return out.toByteArray();
+               }
+       }
+
+       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) {
+                               //
+                       }
+       }
+
+       public static String toString(BufferedReader reader) throws IOException {
+               StringJoiner sn = new StringJoiner("\n");
+               String line = null;
+               while ((line = reader.readLine()) != null)
+                       sn.add(line);
+               return sn.toString();
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/Tester.java b/org.argeo.cms/src/org/argeo/cms/util/Tester.java
new file mode 100644 (file)
index 0000000..fa62cd7
--- /dev/null
@@ -0,0 +1,126 @@
+package org.argeo.cms.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]";
+
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/TesterStatus.java b/org.argeo.cms/src/org/argeo/cms/util/TesterStatus.java
new file mode 100644 (file)
index 0000000..09ab432
--- /dev/null
@@ -0,0 +1,98 @@
+package org.argeo.cms.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");
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/Throughput.java b/org.argeo.cms/src/org/argeo/cms/util/Throughput.java
new file mode 100644 (file)
index 0000000..4fc15f9
--- /dev/null
@@ -0,0 +1,82 @@
+package org.argeo.cms.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;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/util/package-info.java b/org.argeo.cms/src/org/argeo/cms/util/package-info.java
new file mode 100644 (file)
index 0000000..5efc68a
--- /dev/null
@@ -0,0 +1,2 @@
+/** Generic Java utilities. */
+package org.argeo.cms.util;
\ No newline at end of file
diff --git a/org.argeo.util/.classpath b/org.argeo.util/.classpath
deleted file mode 100644 (file)
index 4199cd3..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<?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-17">
-               <attributes>
-                       <attribute name="module" value="true"/>
-               </attributes>
-       </classpathentry>
-       <classpathentry kind="output" path="bin"/>
-</classpath>
diff --git a/org.argeo.util/.project b/org.argeo.util/.project
deleted file mode 100644 (file)
index 171ff88..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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>
diff --git a/org.argeo.util/META-INF/.gitignore b/org.argeo.util/META-INF/.gitignore
deleted file mode 100644 (file)
index 4854a41..0000000
+++ /dev/null
@@ -1 +0,0 @@
-/MANIFEST.MF
diff --git a/org.argeo.util/bnd.bnd b/org.argeo.util/bnd.bnd
deleted file mode 100644 (file)
index 5f42f77..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-Bundle-Activator: org.argeo.osgi.internal.EnterpriseActivator
-Bundle-ActivationPolicy: lazy
-
-Import-Package:        org.osgi.*;version=0.0.0,\
-!org.apache.commons.logging,\
-*                              
diff --git a/org.argeo.util/build.properties b/org.argeo.util/build.properties
deleted file mode 100644 (file)
index ae2abc5..0000000
+++ /dev/null
@@ -1 +0,0 @@
-source.. = src/
\ No newline at end of file
diff --git a/org.argeo.util/src/org/argeo/osgi/internal/EnterpriseActivator.java b/org.argeo.util/src/org/argeo/osgi/internal/EnterpriseActivator.java
deleted file mode 100644 (file)
index bb495dd..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-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 {
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java b/org.argeo.util/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java
deleted file mode 100644 (file)
index f5e8589..0000000
+++ /dev/null
@@ -1,122 +0,0 @@
-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();
-       }
-
-       @SuppressWarnings("rawtypes")
-       @Override
-       public synchronized void setInformation(Dictionary info) {
-               map.clear();
-               addInformation(info);
-       }
-
-       @SuppressWarnings({ "rawtypes", "unchecked" })
-       @Override
-       public synchronized void addInformation(Dictionary 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();
-               }
-
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/provisioning/package-info.java b/org.argeo.util/src/org/argeo/osgi/provisioning/package-info.java
deleted file mode 100644 (file)
index 1859887..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/** OSGi provisioning support. */
-package org.argeo.osgi.provisioning;
\ No newline at end of file
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java
deleted file mode 100644 (file)
index 05ba948..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-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;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java
deleted file mode 100644 (file)
index c9479d5..0000000
+++ /dev/null
@@ -1,328 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import static org.argeo.osgi.useradmin.DirectoryUserAdmin.toLdapName;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.TreeSet;
-
-import javax.naming.InvalidNameException;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.util.directory.DirectoryConf;
-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 DirectoryUserAdmin systemRoles = null;
-       private DirectoryUserAdmin tokens = null;
-       private Map<LdapName, DirectoryUserAdmin> businessRoles = new HashMap<LdapName, DirectoryUserAdmin>();
-
-       // TODO rather use an empty constructor and an init method
-       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 IllegalStateException("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;
-       }
-
-       /** Builds an authorisation by scanning all referentials. */
-       @Override
-       public Authorization getAuthorization(User user) {
-               if (user == null) {// anonymous
-                       return systemRoles.getAuthorization(null);
-               }
-               DirectoryUserAdmin userReferentialOfThisUser = findUserAdmin(user.getName());
-               Authorization rawAuthorization = userReferentialOfThisUser.getAuthorization(user);
-               User retrievedUser = (User) userReferentialOfThisUser.getRole(user.getName());
-               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
-               List<String> rawRoles = Arrays.asList(rawAuthorization.getRoles());
-               List<String> allRoles = new ArrayList<>(rawRoles);
-               for (LdapName otherBaseDn : businessRoles.keySet()) {
-                       if (otherBaseDn.equals(userReferentialOfThisUser.getBaseDn()))
-                               continue;
-                       DirectoryUserAdmin otherUserAdmin = userAdminToUse(user, businessRoles.get(otherBaseDn));
-                       if (otherUserAdmin == null)
-                               continue;
-                       for (String roleStr : rawRoles) {
-                               User role = (User) findUserAdmin(roleStr).getRole(roleStr);
-                               Authorization auth = otherUserAdmin.getAuthorization(role);
-                               allRoles.addAll(Arrays.asList(auth.getRoles()));
-                       }
-
-               }
-
-               // integrate system roles
-               final DirectoryUserAdmin userAdminToUse = userAdminToUse(retrievedUser, userReferentialOfThisUser);
-               Objects.requireNonNull(userAdminToUse);
-
-               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,
-                                       allRoles.toArray(new String[allRoles.size()]));
-                       return authorization;
-               } finally {
-                       if (userAdminToUse != null && userAdminToUse.isScoped()) {
-                               userAdminToUse.destroy();
-                       }
-               }
-       }
-
-       /** Decide whether to scope or not */
-       private DirectoryUserAdmin userAdminToUse(User user, DirectoryUserAdmin userAdmin) {
-               if (userAdmin.isAuthenticated())
-                       return userAdmin;
-               if (user instanceof DirectoryUser) {
-                       return userAdmin;
-               } else if (user instanceof AuthenticatingUser) {
-                       return userAdmin.scope(user).orElse(null);
-               } else {
-                       throw new IllegalArgumentException("Unsupported user type " + user.getClass());
-               }
-
-       }
-
-       /**
-        * 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(UserDirectory ud) {
-               if (!(ud instanceof DirectoryUserAdmin))
-                       throw new IllegalArgumentException("Only " + DirectoryUserAdmin.class.getName() + " is supported");
-               DirectoryUserAdmin userDirectory = (DirectoryUserAdmin) ud;
-               String basePath = userDirectory.getBase();
-               if (isSystemRolesBaseDn(basePath)) {
-                       this.systemRoles = userDirectory;
-                       systemRoles.setExternalRoles(this);
-               } else if (isTokensBaseDn(basePath)) {
-                       this.tokens = userDirectory;
-                       tokens.setExternalRoles(this);
-               } else {
-                       LdapName baseDn = toLdapName(basePath);
-                       if (businessRoles.containsKey(baseDn))
-                               throw new IllegalStateException("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(UserDirectory userDirectory) {
-       }
-
-       private DirectoryUserAdmin findUserAdmin(String name) {
-               try {
-                       return findUserAdmin(new LdapName(name));
-               } catch (InvalidNameException e) {
-                       throw new IllegalArgumentException("Badly formatted name " + name, e);
-               }
-       }
-
-       private DirectoryUserAdmin findUserAdmin(LdapName name) {
-               if (name.startsWith(systemRolesBaseDn))
-                       return systemRoles;
-               if (tokensBaseDn != null && name.startsWith(tokensBaseDn))
-                       return tokens;
-               List<DirectoryUserAdmin> res = new ArrayList<>(1);
-               userDirectories: for (LdapName baseDn : businessRoles.keySet()) {
-                       DirectoryUserAdmin 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 IllegalStateException("Cannot find user admin for " + name);
-               if (res.size() > 1)
-                       throw new IllegalStateException("Multiple user admin found for " + name);
-               return res.get(0);
-       }
-
-       protected boolean isSystemRolesBaseDn(String basePath) {
-               return toLdapName(basePath).equals(systemRolesBaseDn);
-       }
-
-       protected boolean isTokensBaseDn(String basePath) {
-               return tokensBaseDn != null && toLdapName(basePath).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 start() {
-               if (systemRoles == null) {
-                       // TODO do we really need separate system roles?
-                       Hashtable<String, Object> properties = new Hashtable<>();
-                       properties.put(DirectoryConf.baseDn.name(), "ou=roles,ou=system");
-                       systemRoles = new DirectoryUserAdmin(properties);
-               }
-       }
-
-       public void stop() {
-               for (LdapName name : businessRoles.keySet()) {
-                       DirectoryUserAdmin userDirectory = businessRoles.get(name);
-                       destroy(userDirectory);
-               }
-               businessRoles.clear();
-               businessRoles = null;
-               destroy(systemRoles);
-               systemRoles = null;
-       }
-
-       private void destroy(DirectoryUserAdmin userDirectory) {
-               preDestroy(userDirectory);
-               userDirectory.destroy();
-       }
-
-//     protected void removeUserDirectory(UserDirectory userDirectory) {
-//             LdapName baseDn = toLdapName(userDirectory.getContext());
-//             businessRoles.remove(baseDn);
-//             if (userDirectory instanceof DirectoryUserAdmin)
-//                     destroy((DirectoryUserAdmin) userDirectory);
-//     }
-
-       @Deprecated
-       protected void removeUserDirectory(String basePath) {
-               if (isSystemRolesBaseDn(basePath))
-                       throw new IllegalArgumentException("System roles cannot be removed ");
-               LdapName baseDn = toLdapName(basePath);
-               if (!businessRoles.containsKey(baseDn))
-                       throw new IllegalStateException("No user directory registered for " + baseDn);
-               DirectoryUserAdmin userDirectory = businessRoles.remove(baseDn);
-               destroy(userDirectory);
-       }
-
-       /**
-        * Called before each user directory is destroyed, so that additional actions
-        * can be performed.
-        */
-       protected void preDestroy(UserDirectory userDirectory) {
-       }
-
-       public Set<UserDirectory> getUserDirectories() {
-               TreeSet<UserDirectory> res = new TreeSet<>((o1, o2) -> o1.getBase().compareTo(o2.getBase()));
-               res.addAll(businessRoles.values());
-               res.add(systemRoles);
-               return res;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java
deleted file mode 100644 (file)
index ba1f3f7..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import java.util.Dictionary;
-import java.util.Hashtable;
-
-import javax.naming.ldap.LdapName;
-
-import org.argeo.util.directory.DirectoryDigestUtils;
-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 = DirectoryDigestUtils.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;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java
deleted file mode 100644 (file)
index 1d58a2d..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import org.osgi.service.useradmin.Group;
-
-/** A group in a user directroy. */
-interface DirectoryGroup extends Group, DirectoryUser {
-//     List<LdapName> getMemberNames();
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java
deleted file mode 100644 (file)
index 18b28a2..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import org.osgi.service.useradmin.User;
-
-/** A user in a user directory. */
-interface DirectoryUser extends User {
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java
deleted file mode 100644 (file)
index fbcff48..0000000
+++ /dev/null
@@ -1,400 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import static org.argeo.util.naming.LdapAttrs.objectClass;
-import static org.argeo.util.naming.LdapObjs.extensibleObject;
-import static org.argeo.util.naming.LdapObjs.inetOrgPerson;
-import static org.argeo.util.naming.LdapObjs.organizationalPerson;
-import static org.argeo.util.naming.LdapObjs.person;
-import static org.argeo.util.naming.LdapObjs.top;
-
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.Dictionary;
-import java.util.Hashtable;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Optional;
-
-import javax.naming.Context;
-import javax.naming.InvalidNameException;
-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 javax.security.auth.Subject;
-import javax.security.auth.kerberos.KerberosTicket;
-
-import org.argeo.util.CurrentSubject;
-import org.argeo.util.directory.DirectoryConf;
-import org.argeo.util.directory.DirectoryDigestUtils;
-import org.argeo.util.directory.HierarchyUnit;
-import org.argeo.util.directory.ldap.AbstractLdapDirectory;
-import org.argeo.util.directory.ldap.LdapDao;
-import org.argeo.util.directory.ldap.LdapEntry;
-import org.argeo.util.directory.ldap.LdapEntryWorkingCopy;
-import org.argeo.util.directory.ldap.LdapNameUtils;
-import org.argeo.util.directory.ldap.LdifDao;
-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 class DirectoryUserAdmin extends AbstractLdapDirectory implements UserAdmin, UserDirectory {
-
-       private UserAdmin externalRoles;
-
-       // Transaction
-       public DirectoryUserAdmin(URI uriArg, Dictionary<String, ?> props) {
-               this(uriArg, props, false);
-       }
-
-       public DirectoryUserAdmin(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
-               super(uriArg, props, scoped);
-       }
-
-       public DirectoryUserAdmin(Dictionary<String, ?> props) {
-               this(null, props);
-       }
-
-       /*
-        * ABSTRACT METHODS
-        */
-
-       protected Optional<DirectoryUserAdmin> scope(User user) {
-               if (getDirectoryDao() instanceof LdapDao) {
-                       return scopeLdap(user);
-               } else if (getDirectoryDao() instanceof LdifDao) {
-                       return scopeLdif(user);
-               } else {
-                       throw new IllegalStateException("Unsupported DAO " + getDirectoryDao().getClass());
-               }
-       }
-
-       protected Optional<DirectoryUserAdmin> scopeLdap(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 = cloneConfigProperties();
-               properties.put(Context.SECURITY_PRINCIPAL, username.toString());
-               Object pwdCred = credentials.get(SHARED_STATE_PASSWORD);
-               byte[] pwd = (byte[]) pwdCred;
-               if (pwd != null) {
-                       char[] password = DirectoryDigestUtils.bytesToChars(pwd);
-                       properties.put(Context.SECURITY_CREDENTIALS, new String(password));
-               } else {
-                       properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
-               }
-               DirectoryUserAdmin scopedDirectory = new DirectoryUserAdmin(null, properties, true);
-               scopedDirectory.init();
-               // check connection
-               if (!scopedDirectory.getDirectoryDao().checkConnection())
-                       return Optional.empty();
-               return Optional.of(scopedDirectory);
-       }
-
-       protected Optional<DirectoryUserAdmin> scopeLdif(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 = DirectoryDigestUtils.bytesToChars(pwd);
-                       User directoryUser = (User) getRole(username);
-                       if (!directoryUser.hasCredential(null, password))
-                               throw new IllegalStateException("Invalid credentials");
-               } else {
-                       throw new IllegalStateException("Password is required");
-               }
-               Dictionary<String, Object> properties = cloneConfigProperties();
-               properties.put(DirectoryConf.readOnly.name(), "true");
-               DirectoryUserAdmin scopedUserAdmin = new DirectoryUserAdmin(null, properties, true);
-               // FIXME do it better
-               ((LdifDao) getDirectoryDao()).scope((LdifDao) scopedUserAdmin.getDirectoryDao());
-               // no need to check authentication
-               scopedUserAdmin.init();
-               return Optional.of(scopedUserAdmin);
-       }
-
-       @Override
-       public String getRolePath(Role role) {
-               return nameToRelativePath(LdapNameUtils.toLdapName(role.getName()));
-       }
-
-       @Override
-       public String getRoleSimpleName(Role role) {
-               LdapName dn = LdapNameUtils.toLdapName(role.getName());
-               String name = LdapNameUtils.getLastRdnValue(dn);
-               return name;
-       }
-
-       @Override
-       public Role getRoleByPath(String path) {
-               LdapEntry entry = doGetRole(pathToName(path));
-               if (!(entry instanceof Role)) {
-                       return null;
-//                     throw new IllegalStateException("Path must be a UserAdmin Role.");
-               } else {
-                       return (Role) entry;
-               }
-       }
-
-       protected List<Role> getAllRoles(DirectoryUser user) {
-               List<Role> allRoles = new ArrayList<Role>();
-               if (user != null) {
-                       collectRoles((LdapEntry) user, allRoles);
-                       allRoles.add(user);
-               } else
-                       collectAnonymousRoles(allRoles);
-               return allRoles;
-       }
-
-       private void collectRoles(LdapEntry user, List<Role> allRoles) {
-               List<LdapEntry> allEntries = new ArrayList<>();
-               LdapEntry entry = user;
-               collectGroups(entry, allEntries);
-               for (LdapEntry e : allEntries) {
-                       if (e instanceof Role)
-                               allRoles.add((Role) e);
-               }
-       }
-
-       private void collectAnonymousRoles(List<Role> allRoles) {
-               // TODO gather anonymous roles
-       }
-
-       // USER ADMIN
-       @Override
-       public Role getRole(String name) {
-               return (Role) doGetRole(toLdapName(name));
-       }
-
-       @Override
-       public Role[] getRoles(String filter) throws InvalidSyntaxException {
-               List<? extends Role> res = getRoles(getBaseDn(), filter, true);
-               return res.toArray(new Role[res.size()]);
-       }
-
-       List<DirectoryUser> getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException {
-               LdapEntryWorkingCopy wc = getWorkingCopy();
-//             Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
-               List<LdapEntry> searchRes = getDirectoryDao().doGetEntries(searchBase, filter, deep);
-               List<DirectoryUser> res = new ArrayList<>();
-               for (LdapEntry entry : searchRes)
-                       res.add((DirectoryUser) entry);
-               if (wc != null) {
-                       for (Iterator<DirectoryUser> it = res.iterator(); it.hasNext();) {
-                               DirectoryUser user = (DirectoryUser) it.next();
-                               LdapName dn = LdapNameUtils.toLdapName(user.getName());
-                               if (wc.getDeletedData().containsKey(dn))
-                                       it.remove();
-                       }
-                       Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
-                       for (LdapEntry ldapEntry : wc.getNewData().values()) {
-                               DirectoryUser user = (DirectoryUser) ldapEntry;
-                               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;
-       }
-
-       @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 IllegalArgumentException("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) {
-               String f = "(" + key + "=" + value + ")";
-               List<LdapEntry> users = getDirectoryDao().doGetEntries(getBaseDn(), f, true);
-               for (LdapEntry entry : users)
-                       collectedUsers.add((DirectoryUser) entry);
-       }
-
-       @Override
-       public Authorization getAuthorization(User user) {
-               if (user == null) {// anonymous
-                       return new LdifAuthorization(user, getAllRoles(null));
-               }
-               LdapName userName = toLdapName(user.getName());
-               if (isExternal(userName) && user instanceof LdapEntry) {
-                       List<Role> allRoles = new ArrayList<Role>();
-                       collectRoles((LdapEntry) user, allRoles);
-                       return new LdifAuthorization(user, allRoles);
-               } else {
-
-                       Subject currentSubject = CurrentSubject.current();
-                       if (currentSubject != null //
-                                       && getRealm().isPresent() //
-                                       && !currentSubject.getPrivateCredentials(Authorization.class).isEmpty() //
-                                       && !currentSubject.getPrivateCredentials(KerberosTicket.class).isEmpty()) //
-                       {
-                               // TODO not only Kerberos but also bind scope with kept password ?
-                               Authorization auth = currentSubject.getPrivateCredentials(Authorization.class).iterator().next();
-                               // bind with authenticating user
-                               DirectoryUserAdmin scopedUserAdmin = CurrentSubject.callAs(currentSubject, () -> {
-                                       return scope(new AuthenticatingUser(auth.getName(), new Hashtable<>())).orElseThrow();
-                               });
-                               return getAuthorizationFromScoped(scopedUserAdmin, user);
-                       }
-
-                       if (user instanceof DirectoryUser) {
-                               return new LdifAuthorization(user, getAllRoles((DirectoryUser) user));
-                       } else {
-                               // bind with authenticating user
-                               DirectoryUserAdmin scopedUserAdmin = scope(user).orElseThrow();
-                               return getAuthorizationFromScoped(scopedUserAdmin, user);
-                       }
-               }
-       }
-
-       private Authorization getAuthorizationFromScoped(DirectoryUserAdmin scopedUserAdmin, User user) {
-               try {
-                       DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName());
-                       if (directoryUser == null)
-                               throw new IllegalStateException("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();
-               LdapEntryWorkingCopy wc = getWorkingCopy();
-               LdapName dn = toLdapName(name);
-               if ((getDirectoryDao().entryExists(dn) && !wc.getDeletedData().containsKey(dn))
-                               || wc.getNewData().containsKey(dn))
-                       throw new IllegalArgumentException("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.getDeletedData().containsKey(dn)) {
-                       wc.getDeletedData().remove(dn);
-                       wc.getModifiedData().put(dn, attrs);
-                       return getRole(name);
-               } else {
-                       wc.getModifiedData().put(dn, attrs);
-                       LdapEntry newRole = doCreateRole(dn, type, attrs);
-                       wc.getNewData().put(dn, newRole);
-                       return (Role) newRole;
-               }
-       }
-
-       private LdapEntry doCreateRole(LdapName dn, int type, Attributes attrs) {
-               LdapEntry newRole;
-               BasicAttribute objClass = new BasicAttribute(objectClass.name());
-               if (type == Role.USER) {
-                       String userObjClass = getUserObjectClass();
-                       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 = newUser(dn);
-               } else if (type == Role.GROUP) {
-                       String groupObjClass = getGroupObjectClass();
-                       objClass.add(groupObjClass);
-                       // objClass.add(LdifName.extensibleObject.name());
-                       objClass.add(top.name());
-                       attrs.put(objClass);
-                       newRole = newGroup(dn);
-               } else
-                       throw new IllegalArgumentException("Unsupported type " + type);
-               return newRole;
-       }
-
-       @Override
-       public boolean removeRole(String name) {
-               return removeEntry(LdapNameUtils.toLdapName(name));
-       }
-
-       /*
-        * HIERARCHY
-        */
-       @Override
-       public HierarchyUnit getHierarchyUnit(Role role) {
-               LdapName dn = LdapNameUtils.toLdapName(role.getName());
-               LdapName huDn = LdapNameUtils.getParent(dn);
-               HierarchyUnit hierarchyUnit = getDirectoryDao().doGetHierarchyUnit(huDn);
-               if (hierarchyUnit == null)
-                       throw new IllegalStateException("No hierarchy unit found for " + role);
-               return hierarchyUnit;
-       }
-
-       @Override
-       public Iterable<? extends Role> getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep) {
-               LdapName dn = LdapNameUtils.toLdapName(hierarchyUnit.getBase());
-               try {
-                       return getRoles(dn, filter, deep);
-               } catch (InvalidSyntaxException e) {
-                       throw new IllegalArgumentException("Cannot filter " + filter + " " + dn, e);
-               }
-       }
-
-       /*
-        * ROLES CREATION
-        */
-       protected LdapEntry newUser(LdapName name) {
-               // TODO support devices, applications, etc.
-               return new LdifUser(this, name);
-       }
-
-       protected LdapEntry newGroup(LdapName name) {
-               return new LdifGroup(this, name);
-
-       }
-
-       // GETTERS
-       protected UserAdmin getExternalRoles() {
-               return externalRoles;
-       }
-
-       public void setExternalRoles(UserAdmin externalRoles) {
-               this.externalRoles = externalRoles;
-       }
-
-       /*
-        * STATIC UTILITIES
-        */
-       static LdapName toLdapName(String name) {
-               try {
-                       return new LdapName(name);
-               } catch (InvalidNameException e) {
-                       throw new IllegalArgumentException(name + " is not an LDAP name", e);
-               }
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifAuthorization.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifAuthorization.java
deleted file mode 100644 (file)
index d7f6ad9..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Dictionary;
-import java.util.List;
-
-import org.argeo.util.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.name());
-               if (displayName == null)
-                       displayName = props.get(LdapAttrs.cn.name());
-               if (displayName == null)
-                       displayName = props.get(LdapAttrs.uid.name());
-               if (displayName == null)
-                       displayName = user.getName();
-               if (displayName == null)
-                       throw new IllegalStateException("Cannot set display name for " + user);
-               return displayName.toString();
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java
deleted file mode 100644 (file)
index bdf34aa..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.naming.InvalidNameException;
-import javax.naming.directory.Attribute;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.util.directory.ldap.AbstractLdapDirectory;
-import org.osgi.service.useradmin.Role;
-
-/** Directory group implementation */
-class LdifGroup extends LdifUser implements DirectoryGroup {
-       private final String memberAttributeId;
-
-       LdifGroup(AbstractLdapDirectory userAdmin, LdapName dn) {
-               super(userAdmin, dn);
-               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 : getReferences(memberAttributeId)) {
-                       Role role = findRole(ldapName);
-                       if (role == null) {
-                               throw new IllegalStateException("Role " + ldapName + " not found.");
-                       }
-                       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 (NamingException e) {
-//                     throw new IllegalStateException("Cannot get members", e);
-//             }
-//     }
-
-       @Override
-       public Role[] getRequiredMembers() {
-               throw new UnsupportedOperationException();
-       }
-
-       @Override
-       public int getType() {
-               return GROUP;
-       }
-
-       protected DirectoryUserAdmin getUserAdmin() {
-               return (DirectoryUserAdmin) getDirectory();
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java
deleted file mode 100644 (file)
index 0b07c75..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import javax.naming.ldap.LdapName;
-
-import org.argeo.util.directory.ldap.AbstractLdapDirectory;
-import org.argeo.util.directory.ldap.DefaultLdapEntry;
-
-/** Directory user implementation */
-class LdifUser extends DefaultLdapEntry implements DirectoryUser {
-       LdifUser(AbstractLdapDirectory userAdmin, LdapName dn) {
-               super(userAdmin, dn);
-       }
-
-       @Override
-       public String getName() {
-               return getDn().toString();
-       }
-
-       @Override
-       public int getType() {
-               return USER;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java
deleted file mode 100644 (file)
index 5d7e97d..0000000
+++ /dev/null
@@ -1,111 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.naming.NameNotFoundException;
-import javax.naming.NamingException;
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.util.directory.HierarchyUnit;
-import org.argeo.util.directory.ldap.AbstractLdapDirectory;
-import org.argeo.util.directory.ldap.AbstractLdapDirectoryDao;
-import org.argeo.util.directory.ldap.LdapEntry;
-import org.argeo.util.directory.ldap.LdapEntryWorkingCopy;
-import org.argeo.util.naming.LdapAttrs;
-
-/** Pseudo user directory to be used when logging in as OS user. */
-public class OsUserDirectory extends AbstractLdapDirectoryDao {
-       private final String osUsername = System.getProperty("user.name");
-       private final LdapName osUserDn;
-       private final LdapEntry osUser;
-
-       public OsUserDirectory(AbstractLdapDirectory directory) {
-               super(directory);
-               try {
-                       osUserDn = new LdapName(LdapAttrs.uid.name() + "=" + osUsername + "," + directory.getUserBaseRdn() + ","
-                                       + directory.getBaseDn());
-//                     Attributes attributes = new BasicAttributes();
-//                     attributes.put(LdapAttrs.uid.name(), osUsername);
-                       osUser = newUser(osUserDn);
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot create system user", e);
-               }
-       }
-
-       @Override
-       public List<LdapName> getDirectGroups(LdapName dn) {
-               return new ArrayList<>();
-       }
-
-       @Override
-       public boolean entryExists(LdapName dn) {
-               return osUserDn.equals(dn);
-       }
-
-       @Override
-       public boolean checkConnection() {
-               return true;
-       }
-
-       @Override
-       public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException {
-               if (osUserDn.equals(key))
-                       return osUser;
-               else
-                       throw new NameNotFoundException("Not an OS role");
-       }
-
-       @Override
-       public List<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
-               List<LdapEntry> res = new ArrayList<>();
-//             if (f == null || f.match(osUser.getProperties()))
-               res.add(osUser);
-               return res;
-       }
-
-       @Override
-       public HierarchyUnit doGetHierarchyUnit(LdapName dn) {
-               return null;
-       }
-
-       @Override
-       public Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
-               return new ArrayList<>();
-       }
-
-       public void prepare(LdapEntryWorkingCopy wc) {
-
-       }
-
-       public void commit(LdapEntryWorkingCopy wc) {
-
-       }
-
-       public void rollback(LdapEntryWorkingCopy wc) {
-
-       }
-
-       @Override
-       public void init() {
-               // TODO Auto-generated method stub
-
-       }
-
-       @Override
-       public void destroy() {
-               // TODO Auto-generated method stub
-
-       }
-
-       @Override
-       public Attributes doGetAttributes(LdapName name) {
-               try {
-                       return doGetEntry(name).getAttributes();
-               } catch (NameNotFoundException e) {
-                       throw new IllegalStateException(name + " doe not exist in " + getDirectory().getBaseDn(), e);
-               }
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserUtils.java
deleted file mode 100644 (file)
index 5d0cbf6..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-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;
-
-/** Log in based on JDK-provided OS integration. */
-public class OsUserUtils {
-       private final static String LOGIN_CONTEXT_USER_NIX = "USER_NIX";
-       private final 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() {
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/TokenUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/TokenUtils.java
deleted file mode 100644 (file)
index 178b4ae..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import static org.argeo.util.naming.LdapAttrs.description;
-import static org.argeo.util.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.util.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 IllegalArgumentException("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();
-//     }
-
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java
deleted file mode 100644 (file)
index 05ed7cf..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.argeo.osgi.useradmin;
-
-import org.argeo.util.directory.Directory;
-import org.argeo.util.directory.HierarchyUnit;
-import org.osgi.service.useradmin.Role;
-
-/** Information about a user directory. */
-public interface UserDirectory extends Directory {
-
-       HierarchyUnit getHierarchyUnit(Role role);
-
-       Iterable<? extends Role> getHierarchyUnitRoles(HierarchyUnit hierarchyUnit, String filter, boolean deep);
-
-       String getRolePath(Role role);
-
-       String getRoleSimpleName(Role role);
-
-       Role getRoleByPath(String path);
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/jaas-os.cfg b/org.argeo.util/src/org/argeo/osgi/useradmin/jaas-os.cfg
deleted file mode 100644 (file)
index da04505..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-USER_NIX {
-    com.sun.security.auth.module.UnixLoginModule requisite; 
-};
-
-USER_NT {
-    com.sun.security.auth.module.NTLoginModule requisite; 
-};
-
diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/package-info.java b/org.argeo.util/src/org/argeo/osgi/useradmin/package-info.java
deleted file mode 100644 (file)
index c108d2c..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/** LDAP and LDIF based OSGi useradmin implementation. */
-package org.argeo.osgi.useradmin;
\ No newline at end of file
diff --git a/org.argeo.util/src/org/argeo/osgi/util/FilterRequirement.java b/org.argeo.util/src/org/argeo/osgi/util/FilterRequirement.java
deleted file mode 100644 (file)
index 31f1d4d..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-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<>();
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/util/OnServiceRegistration.java b/org.argeo.util/src/org/argeo/osgi/util/OnServiceRegistration.java
deleted file mode 100644 (file)
index 5a6760e..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-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;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java b/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java
deleted file mode 100644 (file)
index 5728b90..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-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.Executor;
-import java.util.concurrent.ForkJoinPool;
-
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceRegistration;
-
-public class OsgiRegister {
-       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();
-       }
-
-       public <T> void 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);
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/CompositeString.java b/org.argeo.util/src/org/argeo/util/CompositeString.java
deleted file mode 100644 (file)
index 2f8587d..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-package org.argeo.util;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.StringTokenizer;
-
-/** A name that can be expressed with various conventions. */
-public class CompositeString {
-       public final static Character UNDERSCORE = Character.valueOf('_');
-       public final static Character SPACE = Character.valueOf(' ');
-       public final static Character DASH = Character.valueOf('-');
-
-       private final String[] parts;
-
-       // optimisation
-       private final int hashCode;
-
-       public CompositeString(String str) {
-               Objects.requireNonNull(str, "String cannot be null");
-               if ("".equals(str.trim()))
-                       throw new IllegalArgumentException("String cannot be empty");
-               if (!str.equals(str.trim()))
-                       throw new IllegalArgumentException("String must be trimmed");
-               this.parts = toParts(str);
-               hashCode = hashCode(this.parts);
-       }
-
-       public String toString(char separator, boolean upperCase) {
-               StringBuilder sb = null;
-               for (String part : parts) {
-                       if (sb == null) {
-                               sb = new StringBuilder();
-                       } else {
-                               sb.append(separator);
-                       }
-                       sb.append(upperCase ? part.toUpperCase() : part);
-               }
-               return sb.toString();
-       }
-
-       public String toStringCaml(boolean firstCharUpperCase) {
-               StringBuilder sb = null;
-               for (String part : parts) {
-                       if (sb == null) {// first
-                               sb = new StringBuilder();
-                               sb.append(firstCharUpperCase ? Character.toUpperCase(part.charAt(0)) : part.charAt(0));
-                       } else {
-                               sb.append(Character.toUpperCase(part.charAt(0)));
-                       }
-
-                       if (part.length() > 1)
-                               sb.append(part.substring(1));
-               }
-               return sb.toString();
-       }
-
-       @Override
-       public int hashCode() {
-               return hashCode;
-       }
-
-       @Override
-       public boolean equals(Object obj) {
-               if (obj == null || !(obj instanceof CompositeString))
-                       return false;
-
-               CompositeString other = (CompositeString) obj;
-               return Arrays.equals(parts, other.parts);
-       }
-
-       @Override
-       public String toString() {
-               return toString(DASH, false);
-       }
-
-       public static String[] toParts(String str) {
-               Character separator = null;
-               if (str.indexOf(UNDERSCORE) >= 0) {
-                       checkNo(str, SPACE);
-                       checkNo(str, DASH);
-                       separator = UNDERSCORE;
-               } else if (str.indexOf(DASH) >= 0) {
-                       checkNo(str, SPACE);
-                       checkNo(str, UNDERSCORE);
-                       separator = DASH;
-               } else if (str.indexOf(SPACE) >= 0) {
-                       checkNo(str, DASH);
-                       checkNo(str, UNDERSCORE);
-                       separator = SPACE;
-               }
-
-               List<String> res = new ArrayList<>();
-               if (separator != null) {
-                       StringTokenizer st = new StringTokenizer(str, separator.toString());
-                       while (st.hasMoreTokens()) {
-                               res.add(st.nextToken().toLowerCase());
-                       }
-               } else {
-                       // single
-                       String strLowerCase = str.toLowerCase();
-                       if (str.toUpperCase().equals(str) || strLowerCase.equals(str))
-                               return new String[] { strLowerCase };
-
-                       // CAML
-                       StringBuilder current = null;
-                       for (char c : str.toCharArray()) {
-                               if (Character.isUpperCase(c)) {
-                                       if (current != null)
-                                               res.add(current.toString());
-                                       current = new StringBuilder();
-                               }
-                               if (current == null)// first char is lower case
-                                       current = new StringBuilder();
-                               current.append(Character.toLowerCase(c));
-                       }
-                       res.add(current.toString());
-               }
-               return res.toArray(new String[res.size()]);
-       }
-
-       private static void checkNo(String str, Character c) {
-               if (str.indexOf(c) >= 0) {
-                       throw new IllegalArgumentException("Only one kind of sperator is allowed");
-               }
-       }
-
-       private static int hashCode(String[] parts) {
-               int hashCode = 0;
-               for (String part : parts) {
-                       hashCode = hashCode + part.hashCode();
-               }
-               return hashCode;
-       }
-
-       static boolean smokeTests() {
-               CompositeString plainName = new CompositeString("NAME");
-               assert "name".equals(plainName.toString());
-               assert "NAME".equals(plainName.toString(UNDERSCORE, true));
-               assert "name".equals(plainName.toString(UNDERSCORE, false));
-               assert "name".equals(plainName.toStringCaml(false));
-               assert "Name".equals(plainName.toStringCaml(true));
-
-               CompositeString camlName = new CompositeString("myComplexName");
-
-               assert new CompositeString("my-complex-name").equals(camlName);
-               assert new CompositeString("MY_COMPLEX_NAME").equals(camlName);
-               assert new CompositeString("My complex Name").equals(camlName);
-               assert new CompositeString("MyComplexName").equals(camlName);
-
-               assert "my-complex-name".equals(camlName.toString());
-               assert "MY_COMPLEX_NAME".equals(camlName.toString(UNDERSCORE, true));
-               assert "my_complex_name".equals(camlName.toString(UNDERSCORE, false));
-               assert "myComplexName".equals(camlName.toStringCaml(false));
-               assert "MyComplexName".equals(camlName.toStringCaml(true));
-
-               return CompositeString.class.desiredAssertionStatus();
-       }
-
-       public static void main(String[] args) {
-               System.out.println(smokeTests());
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/CsvParser.java b/org.argeo.util/src/org/argeo/util/CsvParser.java
deleted file mode 100644 (file)
index b903f77..0000000
+++ /dev/null
@@ -1,242 +0,0 @@
-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;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/CsvParserWithLinesAsMap.java b/org.argeo.util/src/org/argeo/util/CsvParserWithLinesAsMap.java
deleted file mode 100644 (file)
index 8eb6e94..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-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);
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/CsvWriter.java b/org.argeo.util/src/org/argeo/util/CsvWriter.java
deleted file mode 100644 (file)
index c3b3a3a..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-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;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/CurrentSubject.java b/org.argeo.util/src/org/argeo/util/CurrentSubject.java
deleted file mode 100644 (file)
index 60ce3cf..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-package org.argeo.util;
-
-import java.security.AccessController;
-import java.security.PrivilegedActionException;
-import java.security.PrivilegedExceptionAction;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CompletionException;
-
-import javax.security.auth.Subject;
-
-/**
- * Prepare evolution of Java APIs introduced in JDK 18, as these static methods
- * will be added to {@link Subject}.
- */
-@SuppressWarnings("removal")
-public class CurrentSubject {
-
-       private final static boolean useThreadLocal = Boolean
-                       .parseBoolean(System.getProperty("jdk.security.auth.subject.useTL"));
-
-       private final static InheritableThreadLocal<Subject> current = new InheritableThreadLocal<>();
-
-       public static Subject current() {
-               if (useThreadLocal) {
-                       return current.get();
-               } else {// legacy
-                       Subject subject = Subject.getSubject(AccessController.getContext());
-                       return subject;
-               }
-       }
-
-       public static <T> T callAs(Subject subject, Callable<T> action) {
-               if (useThreadLocal) {
-                       Subject previous = current();
-                       current.set(subject);
-                       try {
-                               return action.call();
-                       } catch (Exception e) {
-                               throw new CompletionException("Failed to execute action for " + subject, e);
-                       } finally {
-                               current.set(previous);
-                       }
-               } else {// legacy
-                       try {
-                               return Subject.doAs(subject, new PrivilegedExceptionAction<T>() {
-
-                                       @Override
-                                       public T run() throws Exception {
-                                               return action.call();
-                                       }
-
-                               });
-                       } catch (PrivilegedActionException e) {
-                               throw new CompletionException("Failed to execute action for " + subject, e.getCause());
-                       } catch (Exception e) {
-                               throw new CompletionException("Failed to execute action for " + subject, e);
-                       }
-               }
-       }
-
-       /** Singleton. */
-       private CurrentSubject() {
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/DictionaryKeys.java b/org.argeo.util/src/org/argeo/util/DictionaryKeys.java
deleted file mode 100644 (file)
index d17c86f..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-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();
-               }
-
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/DigestUtils.java b/org.argeo.util/src/org/argeo/util/DigestUtils.java
deleted file mode 100644 (file)
index 38b4e70..0000000
+++ /dev/null
@@ -1,202 +0,0 @@
-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);
-                       for (byte[] arr : bytes)
-                               digest.update(arr);
-                       byte[] checksum = digest.digest();
-                       return checksum;
-               } catch (NoSuchAlgorithmException e) {
-                       throw new UnsupportedOperationException("SHA1 is not avalaible", e);
-               }
-       }
-
-       public static byte[] digestAsBytes(String algorithm, byte[]... bytes) {
-               try {
-                       MessageDigest digest = MessageDigest.getInstance(algorithm);
-                       for (byte[] arr : bytes)
-                               digest.update(arr);
-                       byte[] checksum = digest.digest();
-                       return checksum;
-               } catch (NoSuchAlgorithmException e) {
-                       throw new UnsupportedOperationException("Cannot digest with algorithm " + algorithm, e);
-               }
-       }
-
-       public static String digest(String algorithm, byte[]... bytes) {
-               return toHexString(digestAsBytes(algorithm, bytes));
-       }
-
-       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 = toHexString(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 = toHexString(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 = digestAsBytes(algorithm, path, bufferSize);
-               return toHexString(digest);
-       }
-
-       public static byte[] digestAsBytes(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();
-
-       /** Converts a byte array to an hex String. */
-       public static String toHexString(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);
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/DirH.java b/org.argeo.util/src/org/argeo/util/DirH.java
deleted file mode 100644 (file)
index 013897d..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-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.toHexString(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.toHexString(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.digestAsBytes(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();
-               }
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/ExceptionsChain.java b/org.argeo.util/src/org/argeo/util/ExceptionsChain.java
deleted file mode 100644 (file)
index 9f82421..0000000
+++ /dev/null
@@ -1,90 +0,0 @@
-package org.argeo.util;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Serialisable wrapper of a {@link Throwable}. typically to be written as XML
- * or JSON in a server error response.
- */
-public class ExceptionsChain {
-       private List<SystemException> exceptions = new ArrayList<>();
-
-       public ExceptionsChain() {
-       }
-
-       public ExceptionsChain(Throwable exception) {
-               writeException(exception);
-       }
-
-       /** recursive */
-       protected void writeException(Throwable exception) {
-               SystemException systemException = new SystemException(exception);
-               exceptions.add(systemException);
-               Throwable cause = exception.getCause();
-               if (cause != null)
-                       writeException(cause);
-       }
-
-       public List<SystemException> getExceptions() {
-               return exceptions;
-       }
-
-       public void setExceptions(List<SystemException> exceptions) {
-               this.exceptions = exceptions;
-       }
-
-       /** An exception in the chain. */
-       public static class SystemException {
-               private String type;
-               private String message;
-               private List<String> stackTrace;
-
-               public SystemException() {
-               }
-
-               public SystemException(Throwable exception) {
-                       this.type = exception.getClass().getName();
-                       this.message = exception.getMessage();
-                       this.stackTrace = new ArrayList<>();
-                       StackTraceElement[] elems = exception.getStackTrace();
-                       for (int i = 0; i < elems.length; i++)
-                               stackTrace.add("at " + elems[i].toString());
-               }
-
-               public String getType() {
-                       return type;
-               }
-
-               public void setType(String type) {
-                       this.type = type;
-               }
-
-               public String getMessage() {
-                       return message;
-               }
-
-               public void setMessage(String message) {
-                       this.message = message;
-               }
-
-               public List<String> getStackTrace() {
-                       return stackTrace;
-               }
-
-               public void setStackTrace(List<String> stackTrace) {
-                       this.stackTrace = stackTrace;
-               }
-
-               @Override
-               public String toString() {
-                       return "System exception: " + type + ", " + message + ", " + stackTrace;
-               }
-
-       }
-
-       @Override
-       public String toString() {
-               return exceptions.toString();
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/FsUtils.java b/org.argeo.util/src/org/argeo/util/FsUtils.java
deleted file mode 100644 (file)
index cd61b56..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-package org.argeo.util;
-
-import java.io.IOException;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-
-/** Utilities around the standard Java file abstractions. */
-public class FsUtils {
-
-       /** Deletes this path, recursively if needed. */
-       public static void copyDirectory(Path source, Path target) {
-               if (!Files.exists(source) || !Files.isDirectory(source))
-                       throw new IllegalArgumentException(source + " is not a directory");
-               if (Files.exists(target) && !Files.isDirectory(target))
-                       throw new IllegalArgumentException(target + " is not a directory");
-               try {
-                       Files.createDirectories(target);
-                       Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
-
-                               @Override
-                               public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs) throws IOException {
-                                       Path relativePath = source.relativize(directory);
-                                       Path targetDirectory = target.resolve(relativePath);
-                                       if (!Files.exists(targetDirectory))
-                                               Files.createDirectory(targetDirectory);
-                                       return FileVisitResult.CONTINUE;
-                               }
-
-                               @Override
-                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                                       Path relativePath = source.relativize(file);
-                                       Path targetFile = target.resolve(relativePath);
-                                       Files.copy(file, targetFile);
-                                       return FileVisitResult.CONTINUE;
-                               }
-                       });
-               } catch (IOException e) {
-                       throw new RuntimeException("Cannot copy " + source + " to " + target, e);
-               }
-
-       }
-
-       /**
-        * Deletes this path, recursively if needed. Does nothing if the path does not
-        * exist.
-        */
-       public static void delete(Path path) {
-               try {
-                       if (!Files.exists(path))
-                               return;
-                       Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
-                               @Override
-                               public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
-                                       if (e != null)
-                                               throw e;
-                                       Files.delete(directory);
-                                       return FileVisitResult.CONTINUE;
-                               }
-
-                               @Override
-                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                                       Files.delete(file);
-                                       return FileVisitResult.CONTINUE;
-                               }
-                       });
-               } catch (IOException e) {
-                       throw new RuntimeException("Cannot delete " + path, e);
-               }
-       }
-
-       /** Singleton. */
-       private FsUtils() {
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/LangUtils.java b/org.argeo.util/src/org/argeo/util/LangUtils.java
deleted file mode 100644 (file)
index 1aee28c..0000000
+++ /dev/null
@@ -1,331 +0,0 @@
-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.Collection;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.Iterator;
-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 Map} with one key-value pair. Key should not be null,
-//      * but if the value is null, it returns an empty {@link Map}.
-//      * 
-//      * @deprecated Use {@link Collections#singletonMap(Object, Object)} instead.
-//      */
-//     @Deprecated
-//     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;
-       }
-
-       /** Converts a {@link Dictionary} to a {@link Map}. */
-       public static Map<String, Object> dictToMap(Dictionary<String, ?> properties) {
-               if (properties == null) {
-                       return null;
-               }
-               Map<String, Object> res = new HashMap<>(properties.size());
-               Enumeration<String> keys = properties.keys();
-               while (keys.hasMoreElements()) {
-                       String key = keys.nextElement();
-                       res.put(key, properties.get(key));
-               }
-               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;
-       }
-
-       /** Size of an {@link Iterable}, optimised if it is a {@link Collection}. */
-       public static int size(Iterable<?> iterable) {
-               if (iterable instanceof Collection)
-                       return ((Collection<?>) iterable).size();
-
-               int size = 0;
-               for (Iterator<?> it = iterable.iterator(); it.hasNext(); size++)
-                       it.next();
-               return size;
-       }
-
-       public static <T> T getAt(Iterable<T> iterable, int index) {
-               if (iterable instanceof List) {
-                       List<T> lst = ((List<T>) iterable);
-                       if (index >= lst.size())
-                               throw new IllegalArgumentException("Index " + index + " is not available (size is " + lst.size() + ")");
-                       return lst.get(index);
-               }
-               int i = 0;
-               for (Iterator<T> it = iterable.iterator(); it.hasNext(); i++) {
-                       if (i == index)
-                               return it.next();
-                       else
-                               it.next();
-               }
-               throw new IllegalArgumentException("Index " + index + " is not available (size is " + i + ")");
-       }
-
-       /*
-        * 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() {
-
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/OS.java b/org.argeo.util/src/org/argeo/util/OS.java
deleted file mode 100644 (file)
index 174f45b..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-package org.argeo.util;
-
-import java.io.File;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-/** 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/bash", "-l", "-i" };
-               else
-                       return new String[] { "cmd.exe", "/C" };
-       }
-
-       public static long getJvmPid() {
-               return ProcessHandle.current().pid();
-//             String pidAndHost = ManagementFactory.getRuntimeMXBean().getName();
-//             return Integer.parseInt(pidAndHost.substring(0, pidAndHost.indexOf('@')));
-       }
-
-       /**
-        * Get the runtime directory. It will be the environment variable
-        * XDG_RUNTIME_DIR if it is set, or ~/.cache/argeo if not.
-        */
-       public static Path getRunDir() {
-               Path runDir;
-               String xdgRunDir = System.getenv("XDG_RUNTIME_DIR");
-               if (xdgRunDir != null) {
-                       // TODO support multiple names
-                       runDir = Paths.get(xdgRunDir);
-               } else {
-                       runDir = Paths.get(System.getProperty("user.home"), ".cache/argeo");
-               }
-               return runDir;
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/PasswordEncryption.java b/org.argeo.util/src/org/argeo/util/PasswordEncryption.java
deleted file mode 100644 (file)
index c95c787..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-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;
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/ServiceChannel.java b/org.argeo.util/src/org/argeo/util/ServiceChannel.java
deleted file mode 100644 (file)
index 7997384..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-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;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/StreamUtils.java b/org.argeo.util/src/org/argeo/util/StreamUtils.java
deleted file mode 100644 (file)
index 5e4e636..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-package org.argeo.util;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.io.Writer;
-import java.util.StringJoiner;
-
-/** Stream utilities to be used when Apache Commons IO is not available. */
-public 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 byte[] toByteArray(InputStream in) throws IOException {
-               try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
-                       copy(in, out);
-                       return out.toByteArray();
-               }
-       }
-
-       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) {
-                               //
-                       }
-       }
-
-       public static String toString(BufferedReader reader) throws IOException {
-               StringJoiner sn = new StringJoiner("\n");
-               String line = null;
-               while ((line = reader.readLine()) != null)
-                       sn.add(line);
-               return sn.toString();
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/Tester.java b/org.argeo.util/src/org/argeo/util/Tester.java
deleted file mode 100644 (file)
index 31a2be4..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-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]";
-
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/TesterStatus.java b/org.argeo.util/src/org/argeo/util/TesterStatus.java
deleted file mode 100644 (file)
index d1d14ed..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-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");
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/Throughput.java b/org.argeo.util/src/org/argeo/util/Throughput.java
deleted file mode 100644 (file)
index 266ddbc..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-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;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/Directory.java b/org.argeo.util/src/org/argeo/util/directory/Directory.java
deleted file mode 100644 (file)
index 9886589..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.argeo.util.directory;
-
-import java.util.Optional;
-
-import org.argeo.util.transaction.WorkControl;
-
-/** An information directory (typicylly LDAP). */
-public interface Directory extends HierarchyUnit {
-       String getName();
-
-       /** Whether this directory is read only. */
-       boolean isReadOnly();
-
-       /** Whether this directory is disabled. */
-       boolean isDisabled();
-
-       /** The realm (typically Kerberos) of this directory. */
-       Optional<String> getRealm();
-
-       /** Sets the transaction control used by this directory when editing. */
-       void setTransactionControl(WorkControl transactionControl);
-
-       /*
-        * HIERARCHY
-        */
-
-       /** The hierarchy unit at this path. */
-       HierarchyUnit getHierarchyUnit(String path);
-
-       /** Create a new hierarchy unit. */
-       HierarchyUnit createHierarchyUnit(String path);
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/DirectoryConf.java b/org.argeo.util/src/org/argeo/util/directory/DirectoryConf.java
deleted file mode 100644 (file)
index 4450ca4..0000000
+++ /dev/null
@@ -1,246 +0,0 @@
-package org.argeo.util.directory;
-
-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 org.argeo.util.directory.ldap.IpaUtils;
-import org.argeo.util.naming.NamingUtils;
-
-/** Properties used to configure user admins. */
-public enum DirectoryConf {
-       /** Base DN (cannot be configured externally) */
-       baseDn(null),
-
-       /** URI of the underlying resource (cannot be configured externally) */
-       uri(null),
-
-       /** User objectClass */
-       userObjectClass("inetOrgPerson"),
-
-       /** Relative base DN for users */
-       userBase("ou=People"),
-
-       /** Groups objectClass */
-       groupObjectClass("groupOfNames"),
-
-       /** Relative base DN for users */
-       groupBase("ou=Groups"),
-
-       /** Relative base DN for users */
-       systemRoleBase("ou=Roles"),
-
-       /** Read-only source */
-       readOnly(null),
-
-       /** Disabled source */
-       disabled(null),
-
-       /** Authentication realm */
-       realm(null),
-
-       /** Override all passwords with this value (typically for testing purposes) */
-       forcedPassword(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";
-
-       private final static String SECURITY_PRINCIPAL = "java.naming.security.principal";
-       private final static String SECURITY_CREDENTIALS = "java.naming.security.credentials";
-
-       /** The default value. */
-       private Object def;
-
-       DirectoryConf(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 DirectoryConf local(String property) {
-               return DirectoryConf.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 (DirectoryConf key : DirectoryConf.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 IllegalArgumentException("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 IllegalArgumentException("Unsupported scheme " + scheme);
-                       Map<String, List<String>> query = NamingUtils.queryToMap(u);
-                       for (String key : query.keySet()) {
-                               DirectoryConf ldapProp = DirectoryConf.valueOf(key);
-                               List<String> values = query.get(key);
-                               if (values.size() == 1) {
-                                       res.put(ldapProp.name(), values.get(0));
-                               } else {
-                                       throw new IllegalArgumentException("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(SECURITY_PRINCIPAL, principal);
-                       if (credentials != null)
-                               res.put(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 (URISyntaxException e) {
-                       throw new IllegalArgumentException("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 IllegalStateException("No baseDn in " + properties);
-               return DirectoryDigestUtils.sha1str(bDn);
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/DirectoryDigestUtils.java b/org.argeo.util/src/org/argeo/util/directory/DirectoryDigestUtils.java
deleted file mode 100644 (file)
index d07d2d2..0000000
+++ /dev/null
@@ -1,114 +0,0 @@
-package org.argeo.util.directory;
-
-import java.math.BigInteger;
-import java.nio.ByteBuffer;
-import java.nio.CharBuffer;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.spec.InvalidKeySpecException;
-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. */
-public class DirectoryDigestUtils {
-       public final static String PASSWORD_SCHEME_SHA = "SHA";
-       public final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256";
-
-       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 IllegalStateException("Cannot SHA1 digest", e);
-               }
-       }
-
-       public 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 (NoSuchAlgorithmException | InvalidKeySpecException e) {
-                       throw new IllegalStateException("Cannot digest", e);
-               }
-       }
-
-       public 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;
-       }
-
-       public 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;
-       }
-
-       public 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);
-       }
-
-       /** singleton */
-       private DirectoryDigestUtils() {
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java b/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java
deleted file mode 100644 (file)
index 947b6bc..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.argeo.util.directory;
-
-import java.util.Dictionary;
-import java.util.Locale;
-
-/** A unit within the high-level organisational structure of a directory. */
-public interface HierarchyUnit {
-       /** Name to use in paths. */
-       String getHierarchyUnitName();
-
-       /** Name to use in UI. */
-       String getHierarchyUnitLabel(Locale locale);
-
-       /**
-        * The parent {@link HierarchyUnit}, or <code>null</code> if a
-        * {@link Directory}.
-        */
-       HierarchyUnit getParent();
-
-       /** Direct children {@link HierarchyUnit}s. */
-       Iterable<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly);
-
-       /**
-        * Whether this is an arbitrary named and placed {@link HierarchyUnit}.
-        * 
-        * @return <code>true</code> if functional, <code>false</code> is technical
-        *         (e.g. People, Groups, etc.)
-        */
-       boolean isFunctional();
-
-       /**
-        * The base of this organisational unit within the hierarchy. This would
-        * typically be an LDAP base DN.
-        */
-       String getBase();
-
-       /** The related {@link Directory}. */
-       Directory getDirectory();
-
-       /** Its metadata (typically LDAP attributes). */
-       Dictionary<String, Object> getProperties();
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java
deleted file mode 100644 (file)
index 5cd4ac1..0000000
+++ /dev/null
@@ -1,572 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import static org.argeo.util.directory.ldap.LdapNameUtils.toLdapName;
-
-import java.io.File;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.Arrays;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.Hashtable;
-import java.util.List;
-import java.util.Locale;
-import java.util.Optional;
-import java.util.StringJoiner;
-
-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.BasicAttributes;
-import javax.naming.ldap.LdapName;
-import javax.naming.ldap.Rdn;
-import javax.transaction.xa.XAResource;
-
-import org.argeo.osgi.useradmin.OsUserDirectory;
-import org.argeo.util.directory.Directory;
-import org.argeo.util.directory.DirectoryConf;
-import org.argeo.util.directory.HierarchyUnit;
-import org.argeo.util.naming.LdapAttrs;
-import org.argeo.util.naming.LdapObjs;
-import org.argeo.util.transaction.WorkControl;
-import org.argeo.util.transaction.WorkingCopyXaResource;
-import org.argeo.util.transaction.XAResourceProvider;
-
-/** A {@link Directory} based either on LDAP or LDIF. */
-public abstract class AbstractLdapDirectory implements Directory, XAResourceProvider {
-       protected static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name";
-       protected static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password";
-
-       private final LdapName baseDn;
-       private final Hashtable<String, Object> configProperties;
-       private final Rdn userBaseRdn, groupBaseRdn, systemRoleBaseRdn;
-       private final String userObjectClass, groupObjectClass;
-       private String memberAttributeId = "member";
-
-       private final boolean readOnly;
-       private final boolean disabled;
-       private final String uri;
-
-       private String forcedPassword;
-
-       private final boolean scoped;
-
-       private List<String> credentialAttributeIds = Arrays
-                       .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
-
-       private WorkControl transactionControl;
-       private WorkingCopyXaResource<LdapEntryWorkingCopy> xaResource;
-
-       private LdapDirectoryDao directoryDao;
-
-       /** Whether the the directory has is authenticated via a service user. */
-       private boolean authenticated = false;
-
-       public AbstractLdapDirectory(URI uriArg, Dictionary<String, ?> props, boolean scoped) {
-               this.configProperties = new Hashtable<String, Object>();
-               for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
-                       String key = keys.nextElement();
-                       configProperties.put(key, props.get(key));
-               }
-
-               String baseDnStr = DirectoryConf.baseDn.getValue(configProperties);
-               if (baseDnStr == null)
-                       throw new IllegalArgumentException("Base DN must be specified: " + configProperties);
-               baseDn = toLdapName(baseDnStr);
-               this.scoped = scoped;
-
-               if (uriArg != null) {
-                       uri = uriArg.toString();
-                       // uri from properties is ignored
-               } else {
-                       String uriStr = DirectoryConf.uri.getValue(configProperties);
-                       if (uriStr == null)
-                               uri = null;
-                       else
-                               uri = uriStr;
-               }
-
-               forcedPassword = DirectoryConf.forcedPassword.getValue(configProperties);
-
-               userObjectClass = DirectoryConf.userObjectClass.getValue(configProperties);
-               groupObjectClass = DirectoryConf.groupObjectClass.getValue(configProperties);
-
-               String userBase = DirectoryConf.userBase.getValue(configProperties);
-               String groupBase = DirectoryConf.groupBase.getValue(configProperties);
-               String systemRoleBase = DirectoryConf.systemRoleBase.getValue(configProperties);
-               try {
-//                     baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
-                       userBaseRdn = new Rdn(userBase);
-//                     userBaseDn = new LdapName(userBase + "," + baseDn);
-                       groupBaseRdn = new Rdn(groupBase);
-//                     groupBaseDn = new LdapName(groupBase + "," + baseDn);
-                       systemRoleBaseRdn = new Rdn(systemRoleBase);
-               } catch (InvalidNameException e) {
-                       throw new IllegalArgumentException(
-                                       "Badly formated base DN " + DirectoryConf.baseDn.getValue(configProperties), e);
-               }
-
-               // read only
-               String readOnlyStr = DirectoryConf.readOnly.getValue(configProperties);
-               if (readOnlyStr == null) {
-                       readOnly = readOnlyDefault(uri);
-                       configProperties.put(DirectoryConf.readOnly.name(), Boolean.toString(readOnly));
-               } else
-                       readOnly = Boolean.parseBoolean(readOnlyStr);
-
-               // disabled
-               String disabledStr = DirectoryConf.disabled.getValue(configProperties);
-               if (disabledStr != null)
-                       disabled = Boolean.parseBoolean(disabledStr);
-               else
-                       disabled = false;
-               if (!getRealm().isEmpty()) {
-                       // IPA multiple LDAP causes URI parsing to fail
-                       // TODO manage generic redundant LDAP case
-                       directoryDao = new LdapDao(this);
-               } else {
-                       if (uri != null) {
-                               URI u = URI.create(uri);
-                               if (DirectoryConf.SCHEME_LDAP.equals(u.getScheme())
-                                               || DirectoryConf.SCHEME_LDAPS.equals(u.getScheme())) {
-                                       directoryDao = new LdapDao(this);
-                                       authenticated = configProperties.get(Context.SECURITY_PRINCIPAL) != null;
-                               } else if (DirectoryConf.SCHEME_FILE.equals(u.getScheme())) {
-                                       directoryDao = new LdifDao(this);
-                                       authenticated = true;
-                               } else if (DirectoryConf.SCHEME_OS.equals(u.getScheme())) {
-                                       directoryDao = new OsUserDirectory(this);
-                                       authenticated = true;
-                                       // singleUser = true;
-                               } else {
-                                       throw new IllegalArgumentException("Unsupported scheme " + u.getScheme());
-                               }
-                       } else {
-                               // in memory
-                               directoryDao = new LdifDao(this);
-                       }
-               }
-               if (directoryDao != null)
-                       xaResource = new WorkingCopyXaResource<>(directoryDao);
-       }
-
-       /*
-        * INITIALISATION
-        */
-
-       public void init() {
-               getDirectoryDao().init();
-       }
-
-       public void destroy() {
-               getDirectoryDao().destroy();
-       }
-
-       /*
-        * CREATION
-        */
-       protected abstract LdapEntry newUser(LdapName name);
-
-       protected abstract LdapEntry newGroup(LdapName name);
-
-       /*
-        * EDITION
-        */
-
-       public boolean isEditing() {
-               return xaResource.wc() != null;
-       }
-
-       public LdapEntryWorkingCopy getWorkingCopy() {
-               LdapEntryWorkingCopy wc = xaResource.wc();
-               if (wc == null)
-                       return null;
-               return wc;
-       }
-
-       public void checkEdit() {
-               if (xaResource.wc() == null) {
-                       try {
-                               transactionControl.getWorkContext().registerXAResource(xaResource, null);
-                       } catch (Exception e) {
-                               throw new IllegalStateException("Cannot enlist " + xaResource, e);
-                       }
-               } else {
-               }
-       }
-
-       public void setTransactionControl(WorkControl transactionControl) {
-               this.transactionControl = transactionControl;
-       }
-
-       public XAResource getXaResource() {
-               return xaResource;
-       }
-
-       public boolean removeEntry(LdapName dn) {
-               checkEdit();
-               LdapEntryWorkingCopy wc = getWorkingCopy();
-               boolean actuallyDeleted;
-               if (getDirectoryDao().entryExists(dn) || wc.getNewData().containsKey(dn)) {
-                       LdapEntry user = doGetRole(dn);
-                       wc.getDeletedData().put(dn, user);
-                       actuallyDeleted = true;
-               } else {// just removing from groups (e.g. system roles)
-                       actuallyDeleted = false;
-               }
-               for (LdapName groupDn : getDirectoryDao().getDirectGroups(dn)) {
-                       LdapEntry group = doGetRole(groupDn);
-                       group.getAttributes().get(getMemberAttributeId()).remove(dn.toString());
-               }
-               return actuallyDeleted;
-       }
-
-       /*
-        * RETRIEVAL
-        */
-
-       protected LdapEntry doGetRole(LdapName dn) {
-               LdapEntryWorkingCopy wc = getWorkingCopy();
-               LdapEntry user;
-               try {
-                       user = getDirectoryDao().doGetEntry(dn);
-               } catch (NameNotFoundException e) {
-                       user = null;
-               }
-               if (wc != null) {
-                       if (user == null && wc.getNewData().containsKey(dn))
-                               user = wc.getNewData().get(dn);
-                       else if (wc.getDeletedData().containsKey(dn))
-                               user = null;
-               }
-               return user;
-       }
-
-       protected void collectGroups(LdapEntry user, List<LdapEntry> 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 memberOf
-               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());
-                                       LdapEntry group = doGetRole(groupDn);
-                                       if (group != null) {
-                                               allRoles.add(group);
-                                       } else {
-                                               // user doesn't have the right to retrieve role, but we know it exists
-                                               // otherwise memberOf would not work
-                                               group = newGroup(groupDn);
-                                               allRoles.add(group);
-                                       }
-                               }
-                       } catch (NamingException e) {
-                               throw new IllegalStateException("Cannot get memberOf groups for " + user, e);
-                       }
-               } else {
-                       directGroups: for (LdapName groupDn : getDirectoryDao().getDirectGroups(user.getDn())) {
-                               LdapEntry group = doGetRole(groupDn);
-                               if (group != null) {
-                                       if (allRoles.contains(group)) {
-                                               // important in order to avoi loops
-                                               continue directGroups;
-                                       }
-                                       allRoles.add(group);
-                                       collectGroups(group, allRoles);
-                               }
-                       }
-               }
-       }
-
-       /*
-        * HIERARCHY
-        */
-       @Override
-       public HierarchyUnit getHierarchyUnit(String path) {
-               LdapName dn = pathToName(path);
-               return directoryDao.doGetHierarchyUnit(dn);
-       }
-
-       @Override
-       public Iterable<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly) {
-               return directoryDao.doGetDirectHierarchyUnits(baseDn, functionalOnly);
-       }
-
-       @Override
-       public String getHierarchyUnitName() {
-               return getName();
-       }
-
-       @Override
-       public String getHierarchyUnitLabel(Locale locale) {
-               String key = LdapNameUtils.getLastRdn(getBaseDn()).getType();
-               Object value = LdapEntry.getLocalized(asLdapEntry().getProperties(), key, locale);
-               if (value == null)
-                       value = getHierarchyUnitName();
-               assert value != null;
-               return value.toString();
-       }
-
-       @Override
-       public HierarchyUnit getParent() {
-               return null;
-       }
-
-       @Override
-       public boolean isFunctional() {
-               return true;
-       }
-
-       @Override
-       public Directory getDirectory() {
-               return this;
-       }
-
-       @Override
-       public HierarchyUnit createHierarchyUnit(String path) {
-               checkEdit();
-               LdapEntryWorkingCopy wc = getWorkingCopy();
-               LdapName dn = pathToName(path);
-               if ((getDirectoryDao().entryExists(dn) && !wc.getDeletedData().containsKey(dn))
-                               || wc.getNewData().containsKey(dn))
-                       throw new IllegalArgumentException("Already a hierarchy unit " + path);
-               BasicAttributes attrs = new BasicAttributes(true);
-               attrs.put(LdapAttrs.objectClass.name(), LdapObjs.organizationalUnit.name());
-               Rdn nameRdn = dn.getRdn(dn.size() - 1);
-               // TODO deal with multiple attr RDN
-               attrs.put(nameRdn.getType(), nameRdn.getValue());
-               wc.getModifiedData().put(dn, attrs);
-               LdapHierarchyUnit newHierarchyUnit = new LdapHierarchyUnit(this, dn);
-               wc.getNewData().put(dn, newHierarchyUnit);
-               return newHierarchyUnit;
-       }
-
-       /*
-        * PATHS
-        */
-
-       @Override
-       public String getBase() {
-               return getBaseDn().toString();
-       }
-
-       @Override
-       public String getName() {
-               return nameToSimple(getBaseDn(), ".");
-       }
-
-       protected String nameToRelativePath(LdapName dn) {
-               LdapName name = LdapNameUtils.relativeName(getBaseDn(), dn);
-               return nameToSimple(name, "/");
-       }
-
-       protected String nameToSimple(LdapName name, String separator) {
-               StringJoiner path = new StringJoiner(separator);
-               for (int i = 0; i < name.size(); i++) {
-                       path.add(name.getRdn(i).getValue().toString());
-               }
-               return path.toString();
-
-       }
-
-       protected LdapName pathToName(String path) {
-               try {
-                       LdapName name = (LdapName) getBaseDn().clone();
-                       String[] segments = path.split("/");
-                       Rdn parentRdn = null;
-                       // segments[0] is the directory itself
-                       for (int i = 0; i < segments.length; i++) {
-                               String segment = segments[i];
-                               // TODO make attr names configurable ?
-                               String attr = getDirectory().getRealm().isPresent()/* IPA */ ? LdapAttrs.cn.name()
-                                               : LdapAttrs.ou.name();
-                               if (parentRdn != null) {
-                                       if (getUserBaseRdn().equals(parentRdn))
-                                               attr = LdapAttrs.uid.name();
-                                       else if (getGroupBaseRdn().equals(parentRdn))
-                                               attr = LdapAttrs.cn.name();
-                                       else if (getSystemRoleBaseRdn().equals(parentRdn))
-                                               attr = LdapAttrs.cn.name();
-                               }
-                               Rdn rdn = new Rdn(attr, segment);
-                               name.add(rdn);
-                               parentRdn = rdn;
-                       }
-                       return name;
-               } catch (InvalidNameException e) {
-                       throw new IllegalStateException("Cannot convert " + path + " to LDAP name", e);
-               }
-
-       }
-
-       /*
-        * UTILITIES
-        */
-       protected boolean isExternal(LdapName name) {
-               return !name.startsWith(baseDn);
-       }
-
-       protected static boolean hasObjectClass(Attributes attrs, LdapObjs objectClass) {
-               return hasObjectClass(attrs, objectClass.name());
-       }
-
-       protected static boolean hasObjectClass(Attributes attrs, String objectClass) {
-               try {
-                       Attribute attr = attrs.get(LdapAttrs.objectClass.name());
-                       NamingEnumeration<?> en = attr.getAll();
-                       while (en.hasMore()) {
-                               String v = en.next().toString();
-                               if (v.equalsIgnoreCase(objectClass))
-                                       return true;
-
-                       }
-                       return false;
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot search for objectClass " + objectClass, e);
-               }
-       }
-
-       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(DirectoryConf.SCHEME_FILE)) {
-                       File file = new File(uri);
-                       if (file.exists())
-                               return !file.canWrite();
-                       else
-                               return !file.getParentFile().canWrite();
-               } else if (uri.getScheme().equals(DirectoryConf.SCHEME_LDAP)) {
-                       if (uri.getAuthority() != null)// assume writable if authenticated
-                               return false;
-               } else if (uri.getScheme().equals(DirectoryConf.SCHEME_OS)) {
-                       return true;
-               }
-               return true;// read only by default
-       }
-
-       /*
-        * AS AN ENTRY
-        */
-       public LdapEntry asLdapEntry() {
-               try {
-                       return directoryDao.doGetEntry(baseDn);
-               } catch (NameNotFoundException e) {
-                       throw new IllegalStateException("Cannot get " + baseDn + " entry", e);
-               }
-       }
-
-       public Dictionary<String, Object> getProperties() {
-               return asLdapEntry().getProperties();
-       }
-
-       /*
-        * ACCESSORS
-        */
-       @Override
-       public Optional<String> getRealm() {
-               Object realm = configProperties.get(DirectoryConf.realm.name());
-               if (realm == null)
-                       return Optional.empty();
-               return Optional.of(realm.toString());
-       }
-
-       public LdapName getBaseDn() {
-               return (LdapName) baseDn.clone();
-       }
-
-       public boolean isReadOnly() {
-               return readOnly;
-       }
-
-       public boolean isDisabled() {
-               return disabled;
-       }
-
-       public boolean isAuthenticated() {
-               return authenticated;
-       }
-
-       public Rdn getUserBaseRdn() {
-               return userBaseRdn;
-       }
-
-       public Rdn getGroupBaseRdn() {
-               return groupBaseRdn;
-       }
-
-       public Rdn getSystemRoleBaseRdn() {
-               return systemRoleBaseRdn;
-       }
-
-//     public Dictionary<String, Object> getConfigProperties() {
-//             return configProperties;
-//     }
-
-       public Dictionary<String, Object> cloneConfigProperties() {
-               return new Hashtable<>(configProperties);
-       }
-
-       public String getForcedPassword() {
-               return forcedPassword;
-       }
-
-       public boolean isScoped() {
-               return scoped;
-       }
-
-       public List<String> getCredentialAttributeIds() {
-               return credentialAttributeIds;
-       }
-
-       public String getUri() {
-               return uri;
-       }
-
-       public LdapDirectoryDao getDirectoryDao() {
-               return directoryDao;
-       }
-
-       /** dn can be null, in that case a default should be returned. */
-       public String getUserObjectClass() {
-               return userObjectClass;
-       }
-
-       public String getGroupObjectClass() {
-               return groupObjectClass;
-       }
-
-       public String getMemberAttributeId() {
-               return memberAttributeId;
-       }
-
-       /*
-        * OBJECT METHODS
-        */
-
-       @Override
-       public int hashCode() {
-               return baseDn.hashCode();
-       }
-
-       @Override
-       public String toString() {
-               return "Directory " + baseDn.toString();
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectoryDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectoryDao.java
deleted file mode 100644 (file)
index e6d242f..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import javax.naming.ldap.LdapName;
-
-/** Base class for LDAP/LDIF directory DAOs. */
-public abstract class AbstractLdapDirectoryDao implements LdapDirectoryDao {
-
-       private AbstractLdapDirectory directory;
-
-       public AbstractLdapDirectoryDao(AbstractLdapDirectory directory) {
-               this.directory = directory;
-       }
-
-       public AbstractLdapDirectory getDirectory() {
-               return directory;
-       }
-
-       @Override
-       public LdapEntryWorkingCopy newWorkingCopy() {
-               return new LdapEntryWorkingCopy();
-       }
-
-       @Override
-       public LdapEntry newUser(LdapName name) {
-               return getDirectory().newUser(name);
-       }
-
-       @Override
-       public LdapEntry newGroup(LdapName name) {
-               return getDirectory().newGroup(name);
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AttributesDictionary.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AttributesDictionary.java
deleted file mode 100644 (file)
index 7b0095f..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-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));
-               }
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AuthPassword.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AuthPassword.java
deleted file mode 100644 (file)
index e10f457..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-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.util.naming.LdapAttrs;
-
-/** 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 IllegalStateException("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());
-               }
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java b/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java
deleted file mode 100644 (file)
index c01d1c3..0000000
+++ /dev/null
@@ -1,481 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-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.Iterator;
-import java.util.List;
-import java.util.Objects;
-import java.util.StringJoiner;
-
-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.util.directory.DirectoryDigestUtils;
-import org.argeo.util.naming.LdapAttrs;
-import org.argeo.util.naming.LdapObjs;
-
-/** An entry in an LDAP (or LDIF) directory. */
-public class DefaultLdapEntry implements LdapEntry {
-       private final AbstractLdapDirectory directory;
-
-       private final LdapName dn;
-
-       private AttributeDictionary properties;
-       private AttributeDictionary credentials;
-
-//     private String primaryObjectClass;
-//     private List<String> objectClasses = new ArrayList<>();
-
-       protected DefaultLdapEntry(AbstractLdapDirectory directory, LdapName dn) {
-               Objects.requireNonNull(directory);
-               Objects.requireNonNull(dn);
-               this.directory = directory;
-               this.dn = dn;
-
-               // Object classes
-//             Objects.requireNonNull(initialAttributes);
-//             try {
-//                     NamingEnumeration<?> en = initialAttributes.get(LdapAttrs.objectClass.name()).getAll();
-//                     String first = null;
-//                     attrs: while (en.hasMore()) {
-//                             String v = en.next().toString();
-//                             if (v.equalsIgnoreCase(LdapObjs.top.name()))
-//                                     continue attrs;
-//                             if (first == null)
-//                                     first = v;
-//                             if (v.equalsIgnoreCase(getDirectory().getUserObjectClass()))
-//                                     primaryObjectClass = getDirectory().getUserObjectClass();
-//                             else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass()))
-//                                     primaryObjectClass = getDirectory().getGroupObjectClass();
-//                             objectClasses.add(v);
-//                     }
-//                     if (primaryObjectClass == null) {
-//                             if (first == null)
-//                                     throw new IllegalStateException("Could not find primary object class");
-//                             primaryObjectClass = first;
-//                     }
-//             } catch (NamingException e) {
-//                     throw new IllegalStateException("Cannot find object classes", e);
-//             }
-
-       }
-
-       @Override
-       public LdapName getDn() {
-               return dn;
-       }
-
-       public synchronized Attributes getAttributes() {
-               return isEditing() ? getModifiedAttributes() : getDirectory().getDirectoryDao().doGetAttributes(dn);
-       }
-
-       @Override
-       public List<LdapName> getReferences(String attributeId) {
-               Attribute memberAttribute = getAttributes().get(attributeId);
-               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 (NamingException e) {
-                       throw new IllegalStateException("Cannot get members", e);
-               }
-
-       }
-
-       /** Should only be called from working copy thread. */
-       protected synchronized Attributes getModifiedAttributes() {
-               assert getWc() != null;
-               return getWc().getModifiedData().get(getDn());
-       }
-
-       protected synchronized boolean isEditing() {
-               return getWc() != null && getModifiedAttributes() != null;
-       }
-
-       private synchronized LdapEntryWorkingCopy getWc() {
-               return directory.getWorkingCopy();
-       }
-
-       protected synchronized void startEditing() {
-//             if (frozen)
-//                     throw new IllegalStateException("Cannot edit frozen view");
-               if (directory.isReadOnly())
-                       throw new IllegalStateException("User directory is read-only");
-               assert getModifiedAttributes() == null;
-               getWc().startEditing(this);
-               // modifiedAttributes = (Attributes) publishedAttributes.clone();
-       }
-
-       public synchronized void publishAttributes(Attributes modifiedAttributes) {
-//             publishedAttributes = modifiedAttributes;
-       }
-
-       /*
-        * PROPERTIES
-        */
-       @Override
-       public Dictionary<String, Object> getProperties() {
-               if (properties == null)
-                       properties = new AttributeDictionary(false);
-               return properties;
-       }
-
-       public Dictionary<String, Object> getCredentials() {
-               if (credentials == null)
-                       credentials = new AttributeDictionary(true);
-               return credentials;
-       }
-
-       /*
-        * 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 = DirectoryDigestUtils.bytesToChars(value);
-
-                       if (getDirectory().getForcedPassword() != null
-                                       && getDirectory().getForcedPassword().equals(new String(password)))
-                               return true;
-
-                       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(), DirectoryDigestUtils.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 = DirectoryDigestUtils.bytesToChars((byte[]) value);
-                                       byte[] valueBytes;
-                                       if (DirectoryDigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
-                                               valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null,
-                                                               null);
-                                       } else if (DirectoryDigestUtils.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 = DirectoryDigestUtils.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 */
-       private static byte[] sha1hash(char[] password) {
-               byte[] hashedPassword = ("{SHA}" + Base64.getEncoder()
-                               .encodeToString(DirectoryDigestUtils.sha1(DirectoryDigestUtils.charsToBytes(password))))
-                               .getBytes(StandardCharsets.UTF_8);
-               return hashedPassword;
-       }
-
-       public AbstractLdapDirectory getDirectory() {
-               return directory;
-       }
-
-       public LdapDirectoryDao getDirectoryDao() {
-               return directory.getDirectoryDao();
-       }
-
-       @Override
-       public int hashCode() {
-               return dn.hashCode();
-       }
-
-       @Override
-       public boolean equals(Object obj) {
-               if (this == obj)
-                       return true;
-               if (obj instanceof LdapEntry) {
-                       LdapEntry that = (LdapEntry) obj;
-                       return this.dn.equals(that.getDn());
-               }
-               return false;
-       }
-
-       @Override
-       public String toString() {
-               return dn.toString();
-       }
-
-       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;
-       }
-
-       protected 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 credentials) {
-                       this.attrFilter = getDirectory().getCredentialAttributeIds();
-                       this.includeFilter = credentials;
-                       try {
-                               NamingEnumeration<String> ids = getAttributes().getIDs();
-                               while (ids.hasMore()) {
-                                       String id = ids.next();
-                                       if (credentials && attrFilter.contains(id))
-                                               effectiveKeys.add(id);
-                                       else if (!credentials && !attrFilter.contains(id))
-                                               effectiveKeys.add(id);
-                               }
-                       } catch (NamingException e) {
-                               throw new IllegalStateException("Cannot initialise attribute dictionary", e);
-                       }
-                       if (!credentials)
-                               effectiveKeys.add(LdapAttrs.objectClasses.name());
-               }
-
-               @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 = !key.equals(LdapAttrs.objectClasses.name()) ? getAttributes().get(key.toString())
-                                               : getAttributes().get(LdapAttrs.objectClass.name());
-                               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;
-                               // special case for object class
-                               if (key.equals(LdapAttrs.objectClass.name())) {
-                                       // TODO support multiple object classes
-                                       NamingEnumeration<?> en = attr.getAll();
-                                       String first = null;
-                                       attrs: while (en.hasMore()) {
-                                               String v = en.next().toString();
-                                               if (v.equalsIgnoreCase(LdapObjs.top.name()))
-                                                       continue attrs;
-                                               if (first == null)
-                                                       first = v;
-                                               if (v.equalsIgnoreCase(getDirectory().getUserObjectClass()))
-                                                       return getDirectory().getUserObjectClass();
-                                               else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass()))
-                                                       return getDirectory().getGroupObjectClass();
-                                       }
-                                       if (first != null)
-                                               return first;
-                                       throw new IllegalStateException("Cannot find objectClass in " + value);
-                               } else {
-                                       NamingEnumeration<?> en = attr.getAll();
-                                       StringJoiner values = new StringJoiner("\n");
-                                       while (en.hasMore()) {
-                                               String v = en.next().toString();
-                                               values.add(v);
-                                       }
-                                       return values.toString();
-                               }
-//                             else
-//                                     return value;
-                       } catch (NamingException e) {
-                               throw new IllegalStateException("Cannot get value for attribute " + key, e);
-                       }
-               }
-
-               @Override
-               public Object put(String key, Object value) {
-                       Objects.requireNonNull(value, "Value for key " + key + " is null");
-                       try {
-                               if (key == null) {
-                                       // FIXME remove this "feature", a key should be specified
-                                       // TODO persist to other sources (like PKCS12)
-                                       char[] password = DirectoryDigestUtils.bytesToChars(value);
-                                       byte[] hashedPassword = sha1hash(password);
-                                       return put(LdapAttrs.userPassword.name(), hashedPassword);
-                               }
-                               if (key.startsWith("X-")) {
-                                       return put(LdapAttrs.authPassword.name(), value);
-                               }
-
-                               // start editing
-                               getDirectory().checkEdit();
-                               if (!isEditing())
-                                       startEditing();
-
-                               // object classes special case.
-                               if (key.equals(LdapAttrs.objectClasses.name())) {
-                                       Attribute attribute = new BasicAttribute(LdapAttrs.objectClass.name());
-                                       String[] objectClasses = value.toString().split("\n");
-                                       for (String objectClass : objectClasses) {
-                                               if (objectClass.trim().equals(""))
-                                                       continue;
-                                               attribute.add(objectClass);
-                                       }
-                                       Attribute previousAttribute = getModifiedAttributes().put(attribute);
-                                       if (previousAttribute != null)
-                                               return previousAttribute.get();
-                                       else
-                                               return null;
-                               }
-
-                               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");
-
-                               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 IllegalStateException("Cannot get value for attribute " + key, e);
-                       }
-               }
-
-               @Override
-               public Object remove(Object key) {
-                       getDirectory().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 IllegalStateException("Cannot remove attribute " + key, e);
-                       }
-               }
-
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/IpaUtils.java b/org.argeo.util/src/org/argeo/util/directory/ldap/IpaUtils.java
deleted file mode 100644 (file)
index 99ad6db..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-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 java.util.StringJoiner;
-
-import javax.naming.InvalidNameException;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.util.directory.DirectoryConf;
-import org.argeo.util.naming.LdapAttrs;
-import org.argeo.util.naming.dns.DnsBrowser;
-
-/** Free IPA specific conventions. */
-public class IpaUtils {
-       public final static String IPA_USER_BASE = "cn=users";
-       public final static String IPA_GROUP_BASE = "cn=groups";
-       public final static String IPA_ROLE_BASE = "cn=roles";
-       public final static String IPA_SERVICE_BASE = "cn=services";
-
-       public final static String IPA_ACCOUNTS_BASE = "cn=accounts";
-
-       private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase();
-
-       public final static String IPA_USER_DIRECTORY_CONFIG = DirectoryConf.userBase + "=" + IPA_USER_BASE + "&"
-                       + DirectoryConf.groupBase + "=" + IPA_GROUP_BASE + "&" + DirectoryConf.systemRoleBase + "=" + IPA_ROLE_BASE
-                       + "&" + DirectoryConf.readOnly + "=true";
-
-       @Deprecated
-       static String domainToUserDirectoryConfigPath(String realm) {
-               return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + DirectoryConf.realm.name() + "=" + realm;
-       }
-
-       public static void addIpaConfig(String realm, Dictionary<String, Object> properties) {
-               properties.put(DirectoryConf.baseDn.name(), domainToBaseDn(realm));
-               properties.put(DirectoryConf.realm.name(), realm);
-               properties.put(DirectoryConf.userBase.name(), IPA_USER_BASE);
-               properties.put(DirectoryConf.groupBase.name(), IPA_GROUP_BASE);
-               properties.put(DirectoryConf.systemRoleBase.name(), IPA_ROLE_BASE);
-               properties.put(DirectoryConf.readOnly.name(), Boolean.TRUE.toString());
-       }
-
-       public static String domainToBaseDn(String domain) {
-               String[] dcs = domain.split("\\.");
-               StringJoiner sj = new StringJoiner(",");
-               for (int i = 0; i < dcs.length; i++) {
-                       String dc = dcs[i];
-                       sj.add(LdapAttrs.dc.name() + '=' + dc.toLowerCase());
-               }
-               return IPA_ACCOUNTS_BASE + ',' + sj.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 (IOException e) {
-                       throw new IllegalStateException("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 IllegalStateException("No Kerberos domain available for " + uri);
-               // TODO intergrate CA certificate in truststore
-               // String schemeToUse = SCHEME_LDAPS;
-               String schemeToUse = DirectoryConf.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(DirectoryConf.SCHEME_LDAP) ? true : false);
-                               if (ldapHosts == null || ldapHosts.size() == 0) {
-                                       throw new IllegalStateException("Cannot configure LDAP for IPA " + uri);
-                               } else {
-                                       ldapHostsStr = ldapHosts.get(0);
-                               }
-                       } catch (IOException e) {
-                               throw new IllegalStateException("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 IllegalStateException("Cannot convert IPA uri " + uri, e);
-               }
-
-               Hashtable<String, Object> res = new Hashtable<>();
-               res.put(DirectoryConf.uri.name(), uriStr.toString());
-               addIpaConfig(kerberosRealm, res);
-               return res;
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapConnection.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapConnection.java
deleted file mode 100644 (file)
index 748efe3..0000000
+++ /dev/null
@@ -1,162 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-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.util.naming.LdapAttrs;
-import org.argeo.util.transaction.WorkingCopy;
-
-/** A synchronized wrapper for a single {@link InitialLdapContext}. */
-// TODO implement multiple contexts and connection pooling.
-public class LdapConnection {
-       private InitialLdapContext initialLdapContext = null;
-
-       public 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 (NamingException e) {
-                       throw new IllegalStateException("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);
-               }
-       }
-
-       public synchronized boolean entryExists(LdapName name) throws NamingException {
-               String[] noAttrOID = new String[] { "1.1" };
-               try {
-                       getLdapContext().getAttributes(name, noAttrOID);
-                       return true;
-               } catch (CommunicationException e) {
-                       reconnect();
-                       getLdapContext().getAttributes(name, noAttrOID);
-                       return true;
-               } catch (NameNotFoundException e) {
-                       return false;
-               }
-       }
-
-       public synchronized void prepareChanges(WorkingCopy<?, ?, LdapName> wc) throws NamingException {
-               // make sure connection will work
-               reconnect();
-
-               // delete
-               for (LdapName dn : wc.getDeletedData().keySet()) {
-                       if (!entryExists(dn))
-                               throw new IllegalStateException("User to delete no found " + dn);
-               }
-               // add
-               for (LdapName dn : wc.getNewData().keySet()) {
-                       if (entryExists(dn))
-                               throw new IllegalStateException("User to create found " + dn);
-               }
-               // modify
-               for (LdapName dn : wc.getModifiedData().keySet()) {
-                       if (!wc.getNewData().containsKey(dn) && !entryExists(dn))
-                               throw new IllegalStateException("User to modify not found " + dn);
-               }
-
-       }
-
-//     protected boolean entryExists(LdapName dn) throws NamingException {
-//             try {
-//                     return getAttributes(dn).size() != 0;
-//             } catch (NameNotFoundException e) {
-//                     return false;
-//             }
-//     }
-
-       public synchronized void commitChanges(LdapEntryWorkingCopy wc) throws NamingException {
-               // delete
-               for (LdapName dn : wc.getDeletedData().keySet()) {
-                       getLdapContext().destroySubcontext(dn);
-               }
-               // add
-               for (LdapName dn : wc.getNewData().keySet()) {
-                       LdapEntry user = wc.getNewData().get(dn);
-                       getLdapContext().createSubcontext(dn, user.getAttributes());
-               }
-               // modify
-               for (LdapName dn : wc.getModifiedData().keySet()) {
-                       Attributes modifiedAttrs = wc.getModifiedData().get(dn);
-                       getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs);
-               }
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java
deleted file mode 100644 (file)
index 9157f23..0000000
+++ /dev/null
@@ -1,264 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import static org.argeo.util.naming.LdapAttrs.objectClass;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.naming.AuthenticationNotSupportedException;
-import javax.naming.Binding;
-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.BasicAttributes;
-import javax.naming.directory.SearchControls;
-import javax.naming.directory.SearchResult;
-import javax.naming.ldap.LdapName;
-import javax.naming.ldap.Rdn;
-
-import org.argeo.util.directory.HierarchyUnit;
-import org.argeo.util.naming.LdapAttrs;
-import org.argeo.util.naming.LdapObjs;
-
-/** A user admin based on a LDAP server. */
-public class LdapDao extends AbstractLdapDirectoryDao {
-       private LdapConnection ldapConnection;
-
-       public LdapDao(AbstractLdapDirectory directory) {
-               super(directory);
-       }
-
-       @Override
-       public void init() {
-               ldapConnection = new LdapConnection(getDirectory().getUri().toString(), getDirectory().cloneConfigProperties());
-       }
-
-       public void destroy() {
-               ldapConnection.destroy();
-       }
-
-       @Override
-       public boolean checkConnection() {
-               try {
-                       return ldapConnection.entryExists(getDirectory().getBaseDn());
-               } catch (NamingException e) {
-                       return false;
-               }
-       }
-
-       @Override
-       public boolean entryExists(LdapName dn) {
-               try {
-                       return ldapConnection.entryExists(dn);
-               } catch (NameNotFoundException e) {
-                       return false;
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot check " + dn, e);
-               }
-       }
-
-       @Override
-       public LdapEntry doGetEntry(LdapName name) throws NameNotFoundException {
-//             if (!entryExists(name))
-//                     throw new NameNotFoundException(name + " was not found in " + getDirectory().getBaseDn());
-               try {
-                       Attributes attrs = ldapConnection.getAttributes(name);
-
-                       LdapEntry res;
-                       Rdn technicalRdn = LdapNameUtils.getParentRdn(name);
-                       if (getDirectory().getGroupBaseRdn().equals(technicalRdn)) {
-                               if (attrs.size() == 0) {// exists but not accessible
-                                       attrs = new BasicAttributes();
-                                       attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name());
-                                       attrs.put(LdapAttrs.objectClass.name(), getDirectory().getGroupObjectClass());
-                               }
-                               res = newGroup(name);
-                       } else if (getDirectory().getSystemRoleBaseRdn().equals(technicalRdn)) {
-                               if (attrs.size() == 0) {// exists but not accessible
-                                       attrs = new BasicAttributes();
-                                       attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name());
-                                       attrs.put(LdapAttrs.objectClass.name(), getDirectory().getGroupObjectClass());
-                               }
-                               res = newGroup(name);
-                       } else if (getDirectory().getUserBaseRdn().equals(technicalRdn)) {
-                               if (attrs.size() == 0) {// exists but not accessible
-                                       attrs = new BasicAttributes();
-                                       attrs.put(LdapAttrs.objectClass.name(), LdapObjs.top.name());
-                                       attrs.put(LdapAttrs.objectClass.name(), getDirectory().getUserObjectClass());
-                               }
-                               res = newUser(name);
-                       } else {
-                               res = new DefaultLdapEntry(getDirectory(), name);
-                       }
-                       return res;
-               } catch (NameNotFoundException e) {
-                       throw e;
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot retrieve entry " + name, e);
-               }
-       }
-
-       @Override
-       public Attributes doGetAttributes(LdapName name) {
-               try {
-                       Attributes attrs = ldapConnection.getAttributes(name);
-                       return attrs;
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot get attributes for " + name);
-               }
-       }
-
-       @Override
-       public List<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
-               ArrayList<LdapEntry> res = new ArrayList<>();
-               try {
-                       String searchFilter = f != null ? f.toString()
-                                       : "(|(" + objectClass.name() + "=" + getDirectory().getUserObjectClass() + ")(" + objectClass.name()
-                                                       + "=" + getDirectory().getGroupObjectClass() + "))";
-                       SearchControls searchControls = new SearchControls();
-                       // only attribute needed is objectClass
-                       searchControls.setReturningAttributes(new String[] { objectClass.name() });
-                       // FIXME make one level consistent with deep
-                       searchControls.setSearchScope(deep ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_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);
-                               LdapEntry role;
-                               if (objectClassAttr.contains(getDirectory().getGroupObjectClass())
-                                               || objectClassAttr.contains(getDirectory().getGroupObjectClass().toLowerCase()))
-                                       role = newGroup(dn);
-                               else if (objectClassAttr.contains(getDirectory().getUserObjectClass())
-                                               || objectClassAttr.contains(getDirectory().getUserObjectClass().toLowerCase()))
-                                       role = newUser(dn);
-                               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 (NamingException e) {
-                       throw new IllegalStateException("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
-       public List<LdapName> getDirectGroups(LdapName dn) {
-               List<LdapName> directGroups = new ArrayList<LdapName>();
-               try {
-                       String searchFilter = "(&(" + objectClass + "=" + getDirectory().getGroupObjectClass() + ")("
-                                       + getDirectory().getMemberAttributeId() + "=" + dn + "))";
-
-                       SearchControls searchControls = new SearchControls();
-                       searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
-
-                       LdapName searchBase = getDirectory().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 (NamingException e) {
-                       throw new IllegalStateException("Cannot populate direct members of " + dn, e);
-               }
-       }
-
-       @Override
-       public void prepare(LdapEntryWorkingCopy wc) {
-               try {
-                       ldapConnection.prepareChanges(wc);
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot prepare LDAP", e);
-               }
-       }
-
-       @Override
-       public void commit(LdapEntryWorkingCopy wc) {
-               try {
-                       ldapConnection.commitChanges(wc);
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot commit LDAP", e);
-               }
-       }
-
-       @Override
-       public void rollback(LdapEntryWorkingCopy wc) {
-               // prepare not impacting
-       }
-
-       /*
-        * HIERARCHY
-        */
-
-       @Override
-       public Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
-               List<HierarchyUnit> res = new ArrayList<>();
-               try {
-                       String structuralFilter = functionalOnly ? ""
-                                       : "(" + getDirectory().getUserBaseRdn() + ")(" + getDirectory().getGroupBaseRdn() + ")("
-                                                       + getDirectory().getSystemRoleBaseRdn() + ")";
-                       String searchFilter = "(|(" + objectClass + "=" + LdapObjs.organizationalUnit.name() + ")(" + objectClass
-                                       + "=" + LdapObjs.organization.name() + ")" + structuralFilter + ")";
-
-                       SearchControls searchControls = new SearchControls();
-                       searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
-                       // no attributes needed
-                       searchControls.setReturningAttributes(new String[0]);
-
-                       NamingEnumeration<SearchResult> results = ldapConnection.search(searchBase, searchFilter, searchControls);
-
-                       while (results.hasMoreElements()) {
-                               SearchResult searchResult = (SearchResult) results.nextElement();
-                               LdapName dn = toDn(searchBase, searchResult);
-//                             Attributes attrs = searchResult.getAttributes();
-                               LdapHierarchyUnit hierarchyUnit = new LdapHierarchyUnit(getDirectory(), dn);
-                               if (functionalOnly) {
-                                       if (hierarchyUnit.isFunctional())
-                                               res.add(hierarchyUnit);
-                               } else {
-                                       res.add(hierarchyUnit);
-                               }
-                       }
-                       return res;
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot get direct hierarchy units ", e);
-               }
-       }
-
-       @Override
-       public HierarchyUnit doGetHierarchyUnit(LdapName dn) {
-               try {
-                       if (getDirectory().getBaseDn().equals(dn))
-                               return getDirectory();
-                       if (!dn.startsWith(getDirectory().getBaseDn()))
-                               throw new IllegalArgumentException(dn + " does not start with base DN " + getDirectory().getBaseDn());
-                       if (!ldapConnection.entryExists(dn))
-                               return null;
-                       return new LdapHierarchyUnit(getDirectory(), dn);
-               } catch (NameNotFoundException e) {
-                       return null;
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot get hierarchy unit " + dn, e);
-               }
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java
deleted file mode 100644 (file)
index c70d8c5..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import java.util.List;
-
-import javax.naming.NameNotFoundException;
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.util.directory.HierarchyUnit;
-import org.argeo.util.transaction.WorkingCopyProcessor;
-
-/** Low-level access to an LDAP/LDIF directory. */
-public interface LdapDirectoryDao extends WorkingCopyProcessor<LdapEntryWorkingCopy> {
-       boolean checkConnection();
-
-       boolean entryExists(LdapName dn);
-
-       LdapEntry doGetEntry(LdapName name) throws NameNotFoundException;
-
-       Attributes doGetAttributes(LdapName name);
-
-       List<LdapEntry> doGetEntries(LdapName searchBase, String filter, boolean deep);
-
-       List<LdapName> getDirectGroups(LdapName dn);
-
-       Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly);
-
-       HierarchyUnit doGetHierarchyUnit(LdapName dn);
-
-       LdapEntry newUser(LdapName name);
-
-       LdapEntry newGroup(LdapName name);
-
-       void init();
-
-       void destroy();
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntry.java
deleted file mode 100644 (file)
index 4657c87..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Dictionary;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.StringJoiner;
-import java.util.TreeSet;
-
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.util.naming.LdapAttrs;
-
-/** An LDAP entry. */
-public interface LdapEntry {
-       LdapName getDn();
-
-       Attributes getAttributes();
-
-       void publishAttributes(Attributes modifiedAttributes);
-
-       List<LdapName> getReferences(String attributeId);
-
-       Dictionary<String, Object> getProperties();
-
-       boolean hasCredential(String key, Object value);
-
-       /*
-        * UTILITIES
-        */
-       /**
-        * Convert a collection of object classes to the format expected by an LDAP
-        * backend.
-        */
-       public static void addObjectClasses(Dictionary<String, Object> properties, Collection<String> objectClasses) {
-               String value = properties.get(LdapAttrs.objectClasses.name()).toString();
-               Set<String> currentObjectClasses = new TreeSet<>(Arrays.asList(value.toString().split("\n")));
-               currentObjectClasses.addAll(objectClasses);
-               StringJoiner values = new StringJoiner("\n");
-               currentObjectClasses.forEach((s) -> values.add(s));
-               properties.put(LdapAttrs.objectClasses.name(), values.toString());
-       }
-
-       public static Object getLocalized(Dictionary<String, Object> properties, String key, Locale locale) {
-               if (locale == null)
-                       return null;
-               Object value = null;
-               value = properties.get(key + ";lang-" + locale.getLanguage() + "-" + locale.getCountry());
-               if (value == null)
-                       value = properties.get(key + ";lang-" + locale.getLanguage());
-               return value;
-       }
-
-       public static String toLocalizedKey(String key, Locale locale) {
-               String country = locale.getCountry();
-               if ("".equals(country)) {
-                       return key + ";lang-" + locale.getLanguage();
-               } else {
-                       return key + ";lang-" + locale.getLanguage() + "-" + locale.getCountry();
-               }
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntryWorkingCopy.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapEntryWorkingCopy.java
deleted file mode 100644 (file)
index 381c11b..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.util.transaction.AbstractWorkingCopy;
-
-/** Working copy for a user directory being edited. */
-public class LdapEntryWorkingCopy extends AbstractWorkingCopy<LdapEntry, Attributes, LdapName> {
-       @Override
-       protected LdapName getId(LdapEntry entry) {
-               return entry.getDn();
-       }
-
-       @Override
-       protected Attributes cloneAttributes(LdapEntry entry) {
-               return (Attributes) entry.getAttributes().clone();
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java
deleted file mode 100644 (file)
index 961f2e3..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import java.util.Locale;
-
-import javax.naming.ldap.LdapName;
-import javax.naming.ldap.Rdn;
-
-import org.argeo.util.directory.HierarchyUnit;
-
-/** LDIF/LDAP based implementation of {@link HierarchyUnit}. */
-public class LdapHierarchyUnit extends DefaultLdapEntry implements HierarchyUnit {
-       private final boolean functional;
-
-       public LdapHierarchyUnit(AbstractLdapDirectory directory, LdapName dn) {
-               super(directory, dn);
-
-               Rdn rdn = LdapNameUtils.getLastRdn(dn);
-               functional = !(directory.getUserBaseRdn().equals(rdn) || directory.getGroupBaseRdn().equals(rdn)
-                               || directory.getSystemRoleBaseRdn().equals(rdn));
-       }
-
-       @Override
-       public HierarchyUnit getParent() {
-               return getDirectoryDao().doGetHierarchyUnit(LdapNameUtils.getParent(getDn()));
-       }
-
-       @Override
-       public Iterable<HierarchyUnit> getDirectHierarchyUnits(boolean functionalOnly) {
-               return getDirectoryDao().doGetDirectHierarchyUnits(getDn(), functionalOnly);
-       }
-
-       @Override
-       public boolean isFunctional() {
-               return functional;
-       }
-
-       @Override
-       public String getHierarchyUnitName() {
-               String name = LdapNameUtils.getLastRdnValue(getDn());
-               // TODO check ou, o, etc.
-               return name;
-       }
-
-       @Override
-       public String getHierarchyUnitLabel(Locale locale) {
-               String key = LdapNameUtils.getLastRdn(getDn()).getType();
-               Object value = LdapEntry.getLocalized(getProperties(), key, locale);
-               if (value == null)
-                       value = getHierarchyUnitName();
-               assert value != null;
-               return value.toString();
-       }
-
-       @Override
-       public String getBase() {
-               return getDn().toString();
-       }
-
-       @Override
-       public String toString() {
-               return "Hierarchy Unit " + getDn().toString();
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapNameUtils.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapNameUtils.java
deleted file mode 100644 (file)
index 88d3175..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import javax.naming.InvalidNameException;
-import javax.naming.ldap.LdapName;
-import javax.naming.ldap.Rdn;
-
-/** Utilities to simplify using {@link LdapName}. */
-public class LdapNameUtils {
-
-       public static LdapName relativeName(LdapName prefix, LdapName dn) {
-               try {
-                       if (!dn.startsWith(prefix))
-                               throw new IllegalArgumentException("Prefix " + prefix + " not consistent with " + dn);
-                       LdapName res = (LdapName) dn.clone();
-                       for (int i = 0; i < prefix.size(); i++) {
-                               res.remove(0);
-                       }
-                       return res;
-               } catch (InvalidNameException e) {
-                       throw new IllegalStateException("Cannot find realtive name", e);
-               }
-       }
-
-       public static LdapName getParent(LdapName dn) {
-               try {
-                       LdapName parent = (LdapName) dn.clone();
-                       parent.remove(parent.size() - 1);
-                       return parent;
-               } catch (InvalidNameException e) {
-                       throw new IllegalArgumentException("Cannot get parent of " + dn, e);
-               }
-       }
-
-       public static Rdn getParentRdn(LdapName dn) {
-               if (dn.size() < 2)
-                       throw new IllegalArgumentException(dn + " has no parent");
-               Rdn parentRdn = dn.getRdn(dn.size() - 2);
-               return parentRdn;
-       }
-
-       public static LdapName toLdapName(String distinguishedName) {
-               try {
-                       return new LdapName(distinguishedName);
-               } catch (InvalidNameException e) {
-                       throw new IllegalArgumentException("Cannot parse " + distinguishedName + " as LDAP name", e);
-               }
-       }
-
-       public static Rdn getLastRdn(LdapName dn) {
-               return dn.getRdn(dn.size() - 1);
-       }
-
-       public static String getLastRdnAsString(LdapName dn) {
-               return getLastRdn(dn).toString();
-       }
-
-       public static String getLastRdnValue(String dn) {
-               return getLastRdnValue(toLdapName(dn));
-       }
-
-       public static String getLastRdnValue(LdapName dn) {
-               return getLastRdn(dn).getValue().toString();
-       }
-
-       /** singleton */
-       private LdapNameUtils() {
-
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java
deleted file mode 100644 (file)
index c200faa..0000000
+++ /dev/null
@@ -1,306 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import static org.argeo.util.naming.LdapAttrs.objectClass;
-import static org.argeo.util.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.HashSet;
-import java.util.List;
-import java.util.NavigableMap;
-import java.util.Objects;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-import javax.naming.NameNotFoundException;
-import javax.naming.NamingEnumeration;
-import javax.naming.NamingException;
-import javax.naming.directory.Attributes;
-import javax.naming.ldap.LdapName;
-
-import org.argeo.util.directory.HierarchyUnit;
-import org.argeo.util.naming.LdapObjs;
-import org.osgi.framework.Filter;
-import org.osgi.framework.FrameworkUtil;
-import org.osgi.framework.InvalidSyntaxException;
-import org.osgi.service.useradmin.Role;
-
-/** A user admin based on a LDIF files. */
-public class LdifDao extends AbstractLdapDirectoryDao {
-       private NavigableMap<LdapName, LdapEntry> entries = new TreeMap<>();
-       private NavigableMap<LdapName, LdapHierarchyUnit> hierarchy = new TreeMap<>();
-
-       private NavigableMap<LdapName, Attributes> values = new TreeMap<>();
-
-       public LdifDao(AbstractLdapDirectory directory) {
-               super(directory);
-       }
-
-       public void init() {
-               String uri = getDirectory().getUri();
-               if (uri == null)
-                       return;
-               try {
-                       URI u = new URI(uri);
-                       if (u.getScheme().equals("file")) {
-                               File file = new File(u);
-                               if (!file.exists())
-                                       return;
-                       }
-                       load(u.toURL().openStream());
-               } catch (IOException | URISyntaxException e) {
-                       throw new IllegalStateException("Cannot open URL " + getDirectory().getUri(), e);
-               }
-       }
-
-       public void save() {
-               if (getDirectory().getUri() == null)
-                       return; // ignore
-               if (getDirectory().isReadOnly())
-                       throw new IllegalStateException(
-                                       "Cannot save LDIF user admin: " + getDirectory().getUri() + " is read-only");
-               try (FileOutputStream out = new FileOutputStream(new File(new URI(getDirectory().getUri())))) {
-                       save(out);
-               } catch (IOException | URISyntaxException e) {
-                       throw new IllegalStateException("Cannot save user admin to " + getDirectory().getUri(), e);
-               }
-       }
-
-       public void save(OutputStream out) throws IOException {
-               try {
-                       LdifWriter ldifWriter = new LdifWriter(out);
-                       for (LdapName name : hierarchy.keySet())
-                               ldifWriter.writeEntry(name, hierarchy.get(name).getAttributes());
-                       for (LdapName name : entries.keySet())
-                               ldifWriter.writeEntry(name, entries.get(name).getAttributes());
-               } finally {
-                       out.close();
-               }
-       }
-
-       public void load(InputStream in) {
-               try {
-                       entries.clear();
-                       hierarchy.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 IllegalStateException(key + " has duplicate id " + id);
-                                       lowerCase.add(id);
-                               }
-
-                               values.put(key, attributes);
-
-                               // 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())) {
-                                               entries.put(key, newUser(key));
-                                               break objectClasses;
-                                       } else if (objectClass.toLowerCase().equals(getDirectory().getGroupObjectClass().toLowerCase())) {
-                                               entries.put(key, newGroup(key));
-                                               break objectClasses;
-                                       } else if (objectClass.equalsIgnoreCase(LdapObjs.organizationalUnit.name())) {
-                                               // TODO skip if it does not contain groups or users
-                                               hierarchy.put(key, new LdapHierarchyUnit(getDirectory(), key));
-                                               break objectClasses;
-                                       }
-                               }
-                       }
-
-               } catch (NamingException | IOException e) {
-                       throw new IllegalStateException("Cannot load user admin service from LDIF", e);
-               }
-       }
-
-       public void destroy() {
-//             if (users == null || groups == null)
-               if (entries == null)
-                       throw new IllegalStateException("User directory " + getDirectory().getBaseDn() + " is already destroyed");
-//             users = null;
-//             groups = null;
-               entries = null;
-       }
-
-       /*
-        * USER ADMIN
-        */
-
-       @Override
-       public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException {
-               if (entries.containsKey(key))
-                       return entries.get(key);
-               throw new NameNotFoundException(key + " not persisted");
-       }
-
-       @Override
-       public Attributes doGetAttributes(LdapName name) {
-               if (!values.containsKey(name))
-                       throw new IllegalStateException(name + " doe not exist in " + getDirectory().getBaseDn());
-               return values.get(name);
-       }
-
-       @Override
-       public boolean checkConnection() {
-               return true;
-       }
-
-       @Override
-       public boolean entryExists(LdapName dn) {
-               return entries.containsKey(dn);// || groups.containsKey(dn);
-       }
-
-       @Override
-       public List<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
-               Objects.requireNonNull(searchBase);
-               ArrayList<LdapEntry> res = new ArrayList<>();
-               if (f == null && deep && getDirectory().getBaseDn().equals(searchBase)) {
-                       res.addAll(entries.values());
-               } else {
-                       filterRoles(entries, searchBase, f, deep, res);
-               }
-               return res;
-       }
-
-       private void filterRoles(SortedMap<LdapName, ? extends LdapEntry> map, LdapName searchBase, String f, boolean deep,
-                       List<LdapEntry> res) {
-               // FIXME get rid of OSGi references
-               try {
-                       // TODO reduce map with search base ?
-                       Filter filter = f != null ? FrameworkUtil.createFilter(f) : null;
-                       roles: for (LdapEntry user : map.values()) {
-                               LdapName dn = user.getDn();
-                               if (dn.startsWith(searchBase)) {
-                                       if (!deep && dn.size() != (searchBase.size() + 1))
-                                               continue roles;
-                                       if (filter == null)
-                                               res.add(user);
-                                       else {
-                                               if (user instanceof Role) {
-                                                       if (filter.match(((Role) user).getProperties()))
-                                                               res.add(user);
-                                               }
-                                       }
-                               }
-                       }
-               } catch (InvalidSyntaxException e) {
-                       throw new IllegalArgumentException("Cannot create filter " + f, e);
-               }
-
-       }
-
-       @Override
-       public List<LdapName> getDirectGroups(LdapName dn) {
-               List<LdapName> directGroups = new ArrayList<LdapName>();
-               entries: for (LdapName name : entries.keySet()) {
-                       LdapEntry group;
-                       try {
-                               LdapEntry entry = doGetEntry(name);
-                               if (AbstractLdapDirectory.hasObjectClass(entry.getAttributes(), getDirectory().getGroupObjectClass())) {
-                                       group = entry;
-                               } else {
-                                       continue entries;
-                               }
-                       } catch (NameNotFoundException e) {
-                               throw new IllegalArgumentException("Group " + dn + " not found", e);
-                       }
-                       if (group.getReferences(getDirectory().getMemberAttributeId()).contains(dn)) {
-                               directGroups.add(group.getDn());
-                       }
-               }
-               return directGroups;
-       }
-
-       @Override
-       public void prepare(LdapEntryWorkingCopy wc) {
-               // delete
-               for (LdapName dn : wc.getDeletedData().keySet()) {
-                       if (entries.containsKey(dn))
-                               entries.remove(dn);
-                       else
-                               throw new IllegalStateException("User to delete not found " + dn);
-               }
-               // add
-               for (LdapName dn : wc.getNewData().keySet()) {
-                       LdapEntry user = (LdapEntry) wc.getNewData().get(dn);
-                       if (entries.containsKey(dn))
-                               throw new IllegalStateException("User to create found " + dn);
-                       entries.put(dn, user);
-               }
-               // modify
-               for (LdapName dn : wc.getModifiedData().keySet()) {
-                       Attributes modifiedAttrs = wc.getModifiedData().get(dn);
-                       LdapEntry user;
-                       try {
-                               user = doGetEntry(dn);
-                       } catch (NameNotFoundException e) {
-                               throw new IllegalStateException("User to modify no found " + dn, e);
-                       }
-                       if (user == null)
-                               throw new IllegalStateException("User to modify no found " + dn);
-                       user.publishAttributes(modifiedAttrs);
-               }
-       }
-
-       @Override
-       public void commit(LdapEntryWorkingCopy wc) {
-               save();
-       }
-
-       @Override
-       public void rollback(LdapEntryWorkingCopy wc) {
-               init();
-       }
-
-       /*
-        * HIERARCHY
-        */
-       @Override
-       public HierarchyUnit doGetHierarchyUnit(LdapName dn) {
-               if (getDirectory().getBaseDn().equals(dn))
-                       return getDirectory();
-               return hierarchy.get(dn);
-       }
-
-       @Override
-       public Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
-               List<HierarchyUnit> res = new ArrayList<>();
-               for (LdapName n : hierarchy.keySet()) {
-                       if (n.size() == searchBase.size() + 1) {
-                               if (n.startsWith(searchBase)) {
-                                       HierarchyUnit hu = hierarchy.get(n);
-                                       if (functionalOnly) {
-                                               if (hu.isFunctional())
-                                                       res.add(hu);
-                                       } else {
-                                               res.add(hu);
-                                       }
-                               }
-                       }
-               }
-               return res;
-       }
-
-       public void scope(LdifDao scoped) {
-               scoped.entries = Collections.unmodifiableNavigableMap(entries);
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifParser.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifParser.java
deleted file mode 100644 (file)
index 0022943..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-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.util.naming.LdapAttrs;
-
-/** 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 IllegalStateException(
-                                               "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 IllegalStateException("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
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifWriter.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifWriter.java
deleted file mode 100644 (file)
index a10f169..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import static org.argeo.util.naming.LdapAttrs.DN;
-import static org.argeo.util.naming.LdapAttrs.member;
-import static org.argeo.util.naming.LdapAttrs.objectClass;
-import static org.argeo.util.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;
-
-/** 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 IllegalArgumentException(
-                                               "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 IllegalStateException("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');
-               }
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/SharedSecret.java b/org.argeo.util/src/org/argeo/util/directory/ldap/SharedSecret.java
deleted file mode 100644 (file)
index eaab167..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.argeo.util.directory.ldap;
-
-import java.time.Instant;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-
-import org.argeo.util.naming.NamingUtils;
-
-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());
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/http/HttpHeader.java b/org.argeo.util/src/org/argeo/util/http/HttpHeader.java
deleted file mode 100644 (file)
index 74cf94c..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.argeo.util.http;
-
-/** Standard HTTP headers (including WebDav). */
-public enum HttpHeader {
-       AUTHORIZATION("Authorization"), //
-       WWW_AUTHENTICATE("WWW-Authenticate"), //
-       ALLOW("Allow"), //
-
-       // WebDav
-       DAV("DAV"), //
-       DEPTH("Depth"), //
-       ;
-
-       public final static String BASIC = "Basic";
-       public final static String REALM = "realm";
-       public final static String NEGOTIATE = "Negotiate";
-
-       private final String name;
-
-       private HttpHeader(String headerName) {
-               this.name = headerName;
-       }
-
-       public String getHeaderName() {
-               return name;
-       }
-
-       @Override
-       public String toString() {
-               return getHeaderName();
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/http/HttpMethod.java b/org.argeo.util/src/org/argeo/util/http/HttpMethod.java
deleted file mode 100644 (file)
index 27b4d8f..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.argeo.util.http;
-
-/** Generic HTTP methods. */
-public enum HttpMethod {
-       OPTIONS, //
-       HEAD, //
-       GET, //
-       POST, //
-       PUT, //
-       DELETE, //
-
-       // WebDav
-       PROPFIND, //
-       PROPPATCH, //
-       MKCOL, //
-       MOVE, //
-       COPY, //
-       ;
-}
diff --git a/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java b/org.argeo.util/src/org/argeo/util/http/HttpServerUtils.java
deleted file mode 100644 (file)
index 9127d2c..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-package org.argeo.util.http;
-
-import java.net.URI;
-import java.util.Objects;
-
-import com.sun.net.httpserver.HttpContext;
-import com.sun.net.httpserver.HttpExchange;
-
-public class HttpServerUtils {
-       private final static String SLASH = "/";
-
-       private static String extractPathWithingContext(HttpContext httpContext, String fullPath, boolean startWithSlash) {
-               Objects.requireNonNull(fullPath);
-               String contextPath = httpContext.getPath();
-               if (!fullPath.startsWith(contextPath))
-                       throw new IllegalArgumentException(fullPath + " does not belong to context" + contextPath);
-               String path = fullPath.substring(contextPath.length());
-               // TODO optimise?
-               if (!startWithSlash && path.startsWith(SLASH)) {
-                       path = path.substring(1);
-               } else if (startWithSlash && !path.startsWith(SLASH)) {
-                       path = SLASH + path;
-               }
-               return path;
-       }
-
-       /** Path within the context, NOT starting with a slash. */
-       public static String relativize(HttpExchange exchange) {
-               URI uri = exchange.getRequestURI();
-               HttpContext httpContext = exchange.getHttpContext();
-               return extractPathWithingContext(httpContext, uri.getPath(), false);
-       }
-
-       /** Path within the context, starting with a slash. */
-       public static String subPath(HttpExchange exchange) {
-               URI uri = exchange.getRequestURI();
-               HttpContext httpContext = exchange.getHttpContext();
-               return extractPathWithingContext(httpContext, uri.getPath(), true);
-       }
-
-       /** singleton */
-       private HttpServerUtils() {
-
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/http/HttpStatus.java b/org.argeo.util/src/org/argeo/util/http/HttpStatus.java
deleted file mode 100644 (file)
index 11e0a36..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-package org.argeo.util.http;
-
-/**
- * Standard HTTP response status codes (including WebDav ones).
- * 
- * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status"
- */
-public enum HttpStatus {
-       // Successful responses (200–299)
-       OK(200, "OK"), //
-       NO_CONTENT(204, "No Content"), //
-       MULTI_STATUS(207, "Multi-Status"), // WebDav
-       // Client error responses (400–499)
-       UNAUTHORIZED(401, "Unauthorized"), //
-       FORBIDDEN(403, "Forbidden"), //
-       NOT_FOUND(404, "Not Found"), //
-       // Server error responses (500-599)
-       INTERNAL_SERVER_ERROR(500, "Internal Server Error"), //
-       NOT_IMPLEMENTED(501, "Not Implemented"), //
-       ;
-
-       private final int code;
-       private final String reasonPhrase;
-
-       HttpStatus(int statusCode, String reasonPhrase) {
-               this.code = statusCode;
-               this.reasonPhrase = reasonPhrase;
-       }
-
-       public int getCode() {
-               return code;
-       }
-
-       public String getReasonPhrase() {
-               return reasonPhrase;
-       }
-
-       /**
-        * The status line, as defined by RFC2616.
-        * 
-        * @see "https://www.rfc-editor.org/rfc/rfc2616#section-6.1"
-        */
-       public String getStatusLine(String httpVersion) {
-               return httpVersion + " " + code + " " + reasonPhrase;
-       }
-
-       public static HttpStatus parseStatusLine(String statusLine) {
-               try {
-                       String[] arr = statusLine.split(" ");
-                       int code = Integer.parseInt(arr[1]);
-                       for (HttpStatus status : values()) {
-                               if (status.getCode() == code)
-                                       return status;
-                       }
-               } catch (Exception e) {
-                       throw new IllegalArgumentException("Invalid status line: " + statusLine, e);
-               }
-               throw new IllegalArgumentException("Unkown status code: " + statusLine);
-       }
-
-       @Override
-       public String toString() {
-               return code + " " + reasonPhrase;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/internal/DisplayQName.java b/org.argeo.util/src/org/argeo/util/internal/DisplayQName.java
deleted file mode 100644 (file)
index 6cc39dc..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.argeo.util.internal;
-
-import javax.xml.namespace.QName;
-
-public class DisplayQName extends QName {
-               private static final long serialVersionUID = 2376484886212253123L;
-
-               public DisplayQName(String namespaceURI, String localPart, String prefix) {
-                       super(namespaceURI, localPart, prefix);
-               }
-
-               public DisplayQName(String localPart) {
-                       super(localPart);
-               }
-
-               @Override
-               public String toString() {
-                       String prefix = getPrefix();
-                       assert prefix != null;
-                       return "".equals(prefix) ? getLocalPart() : prefix + ":" + getLocalPart();
-               }
-
-       }
\ No newline at end of file
diff --git a/org.argeo.util/src/org/argeo/util/naming/Distinguished.java b/org.argeo.util/src/org/argeo/util/naming/Distinguished.java
deleted file mode 100644 (file)
index e339ede..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.argeo.util.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;
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.csv b/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.csv
deleted file mode 100644 (file)
index 676d727..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-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
diff --git a/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java b/org.argeo.util/src/org/argeo/util/naming/LdapAttrs.java
deleted file mode 100644 (file)
index 1a6642f..0000000
+++ /dev/null
@@ -1,356 +0,0 @@
-package org.argeo.util.naming;
-
-import java.util.function.Supplier;
-
-import javax.xml.namespace.QName;
-
-import org.argeo.util.internal.DisplayQName;
-
-/**
- * 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, Supplier<String> {
-       /** */
-       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"),
-
-       // RFC 2307bis (partial)
-       /** */
-       uidNumber("1.3.6.1.1.1.1.0", "RFC 2307bis"),
-       /** */
-       gidNumber("1.3.6.1.1.1.1.1", "RFC 2307bis"),
-       /** */
-       homeDirectory("1.3.6.1.1.1.1.3", "RFC 2307bis"),
-       /** */
-       loginShell("1.3.6.1.1.1.1.4", "RFC 2307bis"),
-       /** */
-       memberUid("1.3.6.1.1.1.1.12", "RFC 2307bis"),
-
-       //
-       ;
-
-       public final static String DN = "dn";
-
-//     private final static String LDAP_ = "ldap:";
-
-       private final String oid, spec;
-       private final QName value;
-
-       LdapAttrs(String oid, String spec) {
-               this.oid = oid;
-               this.spec = spec;
-               this.value = new DisplayQName(LdapObjs.LDAP_NAMESPACE_URI, name(), LdapObjs.LDAP_DEFAULT_PREFIX);
-       }
-
-       public QName qName() {
-               return value;
-       }
-
-       @Override
-       public String getID() {
-               return oid;
-       }
-
-       @Override
-       public String getSpec() {
-               return spec;
-       }
-
-       @Deprecated
-       public String property() {
-               return get();
-       }
-
-       @Deprecated
-       public String qualified() {
-               return get();
-       }
-
-       @Override
-       public String get() {
-               return LdapObjs.LDAP_DEFAULT_PREFIX + ":" + name();
-       }
-
-       @Override
-       public final String toString() {
-               // must return the name
-               return name();
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/naming/LdapObjs.csv b/org.argeo.util/src/org/argeo/util/naming/LdapObjs.csv
deleted file mode 100644 (file)
index 3d907cb..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-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
diff --git a/org.argeo.util/src/org/argeo/util/naming/LdapObjs.java b/org.argeo.util/src/org/argeo/util/naming/LdapObjs.java
deleted file mode 100644 (file)
index 995c68c..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-package org.argeo.util.naming;
-
-import java.util.function.Supplier;
-
-import javax.xml.namespace.QName;
-
-import org.argeo.util.internal.DisplayQName;
-
-/**
- * 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, Supplier<String> {
-       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"),
-
-       // RFC 2307bis (partial)
-       /** */
-       posixAccount("1.3.6.1.1.1.2.0", "RFC 2307bis"),
-       /** */
-       posixGroup("1.3.6.1.1.1.2.2", "RFC 2307bis"),
-
-       //
-       ;
-
-       /** MUST be equal to ContentRepository LDAP namespace. */
-       final static String LDAP_NAMESPACE_URI = "http://www.argeo.org/ns/ldap";
-       /** MUST be equal to ContentRepository LDAP prefix. */
-       final static String LDAP_DEFAULT_PREFIX = "ldap";
-
-       private final String oid, spec;
-       private final QName value;
-
-       private LdapObjs(String oid, String spec) {
-               this.oid = oid;
-               this.spec = spec;
-               this.value = new DisplayQName(LDAP_NAMESPACE_URI, name(), LDAP_DEFAULT_PREFIX);
-       }
-
-       public QName qName() {
-               return value;
-       }
-
-       public String getOid() {
-               return oid;
-       }
-
-       public String getSpec() {
-               return spec;
-       }
-
-       @Deprecated
-       public String property() {
-               return get();
-       }
-
-       @Override
-       public String get() {
-               return LdapObjs.LDAP_DEFAULT_PREFIX + ":" + name();
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/naming/NamingUtils.java b/org.argeo.util/src/org/argeo/util/naming/NamingUtils.java
deleted file mode 100644 (file)
index ff4ed31..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-package org.argeo.util.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");
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/naming/NodeOID.java b/org.argeo.util/src/org/argeo/util/naming/NodeOID.java
deleted file mode 100644 (file)
index ea163d6..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.argeo.util.naming;
-
-interface NodeOID {
-       String BASE = "1.3.6.1.4.1" + ".48308" + ".1";
-
-       // uuidgen --md5 --namespace @oid --name 1.3.6.1.4.1.48308
-       String BASE_UUID_V3 = "6869e86b-96b7-3d49-b6ab-ffffc5ad95fb";
-       
-       // uuidgen --sha1 --namespace @oid --name 1.3.6.1.4.1.48308
-       String BASE_UUID_V5 = "58873947-460c-59a6-a7b4-28a97def5f27";
-       
-       // ATTRIBUTE TYPES
-       String ATTRIBUTE_TYPES = BASE + ".4";
-
-       // OBJECT CLASSES
-       String OBJECT_CLASSES = BASE + ".6";
-}
diff --git a/org.argeo.util/src/org/argeo/util/naming/SpecifiedName.java b/org.argeo.util/src/org/argeo/util/naming/SpecifiedName.java
deleted file mode 100644 (file)
index 22f2a2d..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.argeo.util.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();
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java b/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java
deleted file mode 100644 (file)
index 4fba434..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-package org.argeo.util.naming.dns;
-
-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.Objects;
-import java.util.SortedSet;
-import java.util.StringJoiner;
-import java.util.TreeMap;
-import java.util.TreeSet;
-
-import javax.naming.Binding;
-import javax.naming.Context;
-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.DirContext;
-import javax.naming.directory.InitialDirContext;
-
-public class DnsBrowser implements Closeable {
-       private final DirContext initialCtx;
-
-       public DnsBrowser() {
-               this(new ArrayList<>());
-       }
-
-       public DnsBrowser(List<String> dnsServerUrls) {
-               try {
-                       Objects.requireNonNull(dnsServerUrls);
-                       Hashtable<String, Object> env = new Hashtable<>();
-                       env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
-                       if (!dnsServerUrls.isEmpty()) {
-                               boolean specified = false;
-                               StringJoiner providerUrl = new StringJoiner(" ");
-                               for (String dnsUrl : dnsServerUrls) {
-                                       if (dnsUrl != null) {
-                                               providerUrl.add(dnsUrl);
-                                               specified = true;
-                                       }
-                               }
-                               if (specified)
-                                       env.put(Context.PROVIDER_URL, providerUrl.toString());
-                       }
-                       initialCtx = new InitialDirContext(env);
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot initialise DNS borowser.", e);
-               }
-       }
-
-       public Map<String, List<String>> getAllRecords(String name) {
-               try {
-                       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);
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot get allrecords of " + name, e);
-               }
-       }
-
-       /**
-        * 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) {
-               try {
-                       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();
-               } catch (NameNotFoundException e) {
-                       return null;
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot get DNS entry " + recordType + " of " + name, e);
-               }
-       }
-
-       /**
-        * Return records of a given type.
-        */
-       public List<String> getRecords(String name, String recordType) {
-               try {
-                       List<String> res = new ArrayList<String>();
-                       Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType });
-                       Attribute attr = attrs.get(recordType);
-                       addValues(attr, res);
-                       return res;
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot get records " + recordType + " of " + name, e);
-               }
-       }
-
-       /** Ordered, with preferred first. */
-       public List<String> getSrvRecordsAsHosts(String name, boolean withPort) {
-               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) {
-               try {
-                       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);
-               } catch (NamingException e) {
-                       throw new IllegalStateException("Cannot list entries of " + name, e);
-               }
-       }
-
-       @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
diff --git a/org.argeo.util/src/org/argeo/util/naming/dns/SrvRecord.java b/org.argeo.util/src/org/argeo/util/naming/dns/SrvRecord.java
deleted file mode 100644 (file)
index ea6f3cc..0000000
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.argeo.util.naming.dns;
-
-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 : "");
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/naming/package-info.java b/org.argeo.util/src/org/argeo/util/naming/package-info.java
deleted file mode 100644 (file)
index f62af36..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/** Generic naming and LDAP support. */
-package org.argeo.util.naming;
\ No newline at end of file
diff --git a/org.argeo.util/src/org/argeo/util/package-info.java b/org.argeo.util/src/org/argeo/util/package-info.java
deleted file mode 100644 (file)
index 4354b0a..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/** Generic Java utilities. */
-package org.argeo.util;
\ No newline at end of file
diff --git a/org.argeo.util/src/org/argeo/util/register/Component.java b/org.argeo.util/src/org/argeo/util/register/Component.java
deleted file mode 100644 (file)
index 275811e..0000000
+++ /dev/null
@@ -1,333 +0,0 @@
-package org.argeo.util.register;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CompletionStage;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-/**
- * A wrapper for an object, whose dependencies and life cycle can be managed.
- */
-public class Component<I> implements Supplier<I>, Comparable<Component<?>> {
-
-       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 final Map<String, Object> properties;
-
-       private CompletableFuture<Void> activationStarted = null;
-       private CompletableFuture<Void> activated = null;
-
-       private CompletableFuture<Void> deactivationStarted = null;
-       private CompletableFuture<Void> deactivated = null;
-
-       // internal
-       private Set<Dependency<?>> dependants = new HashSet<>();
-
-       private RankingKey rankingKey;
-
-       Component(ComponentRegister register, I instance, Runnable init, Runnable close, Set<Dependency<?>> dependencies,
-                       Set<Class<? super I>> classes, Map<String, Object> properties) {
-               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();
-
-               long serviceId = register.register(this);
-               Map<String, Object> props = new HashMap<>(properties);
-               props.put(RankingKey.SERVICE_ID, serviceId);
-               this.properties = Collections.unmodifiableMap(props);
-               rankingKey = new RankingKey(properties);
-       }
-
-       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());
-       }
-
-       CompletableFuture<Void> getActivated() {
-               return activated;
-       }
-
-       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);
-       }
-
-       @Override
-       public I get() {
-               return instance;
-       }
-
-       @SuppressWarnings("unchecked")
-       public <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);
-       }
-
-       public <T> boolean isPublishedType(Class<T> clss) {
-               return types.containsKey(clss);
-       }
-
-       public Map<String, Object> getProperties() {
-               return properties;
-       }
-
-       @Override
-       public int compareTo(Component<?> o) {
-               return rankingKey.compareTo(rankingKey);
-       }
-
-       @Override
-       public int hashCode() {
-               Long serviceId = (Long) properties.get(RankingKey.SERVICE_ID);
-               if (serviceId != null)
-                       return serviceId.intValue();
-               else
-                       return super.hashCode();
-       }
-
-       @Override
-       public String toString() {
-               List<String> classes = new ArrayList<>();
-               for (Class<?> clss : types.keySet()) {
-                       classes.add(clss.getName());
-               }
-               return "Component " + classes + " " + properties + "";
-       }
-
-       /** A type which has been explicitly exposed by a component. */
-       public static class PublishedType<T> {
-               private Component<? extends T> component;
-               private Class<T> clss;
-
-               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);
-               }
-
-               public Component<?> getPublisher() {
-                       return component;
-               }
-
-               public Class<T> getType() {
-                       return clss;
-               }
-
-               public CompletionStage<T> getValue() {
-                       return value.minimalCompletionStage();
-               }
-       }
-
-       /** Builds a {@link Component}. */
-       public static class Builder<I> implements Supplier<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<>();
-               private final Map<String, Object> properties = new HashMap<>();
-
-               public Builder(I instance) {
-                       this.instance = instance;
-               }
-
-               public Component<I> build(ComponentRegister 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, properties);
-                       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> addActivation(Runnable init) {
-                       if (this.init != null)
-                               throw new IllegalArgumentException("init method is already set");
-                       this.init = init;
-                       return this;
-               }
-
-               public Builder<I> addDeactivation(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 void addProperty(String key, Object value) {
-                       if (properties.containsKey(key))
-                               throw new IllegalStateException("Key " + key + " is already set.");
-                       properties.put(key, value);
-               }
-
-               @Override
-               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 != null ? set : t -> {
-                       };
-                       this.unset = unset != null ? unset : t -> {
-                       };
-               }
-
-               // 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);
-               }
-
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/register/ComponentRegister.java b/org.argeo.util/src/org/argeo/util/register/ComponentRegister.java
deleted file mode 100644 (file)
index d78b6ba..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.argeo.util.register;
-
-import java.util.Map;
-import java.util.SortedSet;
-import java.util.function.Predicate;
-
-/** A register of components which can coordinate their activation. */
-public interface ComponentRegister {
-       long register(Component<?> component);
-
-       <T> SortedSet<Component<? extends T>> find(Class<T> clss, Predicate<Map<String, Object>> filter);
-
-       default <T> Component.PublishedType<T> getSingleton(Class<T> type) {
-               SortedSet<Component<? extends T>> found = find(type, null);
-               if (found.size() == 0)
-                       throw new IllegalStateException("No component found for " + type);
-               return found.first().getType(type);
-       }
-
-       default <T> T getObject(Class<T> clss) {
-               SortedSet<Component<? extends T>> found = find(clss, null);
-               if (found.size() == 0)
-                       return null;
-               return found.first().get();
-       }
-
-       Component<?> get(Object instance);
-
-//     default <T> PublishedType<T> getType(Class<T> clss) {
-//             SortedSet<Component<? extends T>> components = find(clss, null);
-//             if (components.size() == 0)
-//                     return null;
-//             return components.first().getType(clss);
-//     }
-
-       void activate();
-
-       void deactivate();
-
-       boolean isActive();
-
-       void clear();
-}
diff --git a/org.argeo.util/src/org/argeo/util/register/RankingKey.java b/org.argeo.util/src/org/argeo/util/register/RankingKey.java
deleted file mode 100644 (file)
index 7a43e35..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-package org.argeo.util.register;
-
-import java.util.Map;
-import java.util.Objects;
-
-/**
- * Key used to classify and filter available components.
- */
-public class RankingKey implements Comparable<RankingKey> {
-       public final static String SERVICE_PID = "service.pid";
-       public final static String SERVICE_ID = "service.id";
-       public final static String SERVICE_RANKING = "service.ranking";
-
-       private String pid;
-       private Integer ranking = 0;
-       private Long id = 0l;
-
-       public RankingKey(String pid, Integer ranking, Long id) {
-               super();
-               this.pid = pid;
-               this.ranking = ranking;
-               this.id = id;
-       }
-
-       public RankingKey(Map<String, Object> properties) {
-               this.pid = properties.containsKey(SERVICE_PID) ? properties.get(SERVICE_PID).toString() : null;
-               this.ranking = properties.containsKey(SERVICE_RANKING)
-                               ? Integer.parseInt(properties.get(SERVICE_RANKING).toString())
-                               : 0;
-               this.id = properties.containsKey(SERVICE_ID) ? (Long) properties.get(SERVICE_ID) : null;
-       }
-
-       @Override
-       public int hashCode() {
-               Integer result = 0;
-               if (pid != null)
-                       result = +pid.hashCode();
-               if (ranking != null)
-                       result = +ranking;
-               return result;
-       }
-
-       @Override
-       protected Object clone() throws CloneNotSupportedException {
-               return new RankingKey(pid, ranking, id);
-       }
-
-       @Override
-       public String toString() {
-               StringBuilder sb = new StringBuilder("");
-               if (pid != null)
-                       sb.append(pid);
-               if (ranking != null && ranking != 0)
-                       sb.append(' ').append(ranking);
-               return sb.toString();
-       }
-
-       @Override
-       public boolean equals(Object obj) {
-               if (!(obj instanceof RankingKey))
-                       return false;
-               RankingKey other = (RankingKey) obj;
-               return Objects.equals(pid, other.pid) && Objects.equals(ranking, other.ranking) && Objects.equals(id, other.id);
-       }
-
-       @Override
-       public int compareTo(RankingKey o) {
-               if (pid != null && o.pid != null) {
-                       if (pid.equals(o.pid)) {
-                               if (ranking.equals(o.ranking))
-                                       if (id != null && o.id != null)
-                                               return id.compareTo(o.id);
-                                       else
-                                               return 0;
-                               else
-                                       return ranking.compareTo(o.ranking);
-                       } else {
-                               return pid.compareTo(o.pid);
-                       }
-
-               } else {
-               }
-               return -1;
-       }
-
-       public String getPid() {
-               return pid;
-       }
-
-       public Integer getRanking() {
-               return ranking;
-       }
-
-       public Long getId() {
-               return id;
-       }
-
-       public static RankingKey minPid(String pid) {
-               return new RankingKey(pid, Integer.MIN_VALUE, null);
-       }
-
-       public static RankingKey maxPid(String pid) {
-               return new RankingKey(pid, Integer.MAX_VALUE, null);
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/register/SimpleRegister.java b/org.argeo.util/src/org/argeo/util/register/SimpleRegister.java
deleted file mode 100644 (file)
index 7aa9ebe..0000000
+++ /dev/null
@@ -1,124 +0,0 @@
-package org.argeo.util.register;
-
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Predicate;
-
-/** A minimal component register. */
-public class SimpleRegister implements ComponentRegister {
-       private final AtomicBoolean started = new AtomicBoolean(false);
-       private final IdentityHashMap<Object, Component<?>> components = new IdentityHashMap<>();
-       private final AtomicLong nextServiceId = new AtomicLong(0l);
-
-       @Override
-       public long register(Component<?> component) {
-               return registerComponent(component);
-       }
-
-       @SuppressWarnings({ "unchecked" })
-       @Override
-       public synchronized <T> SortedSet<Component<? extends T>> find(Class<T> clss,
-                       Predicate<Map<String, Object>> filter) {
-               SortedSet<Component<? extends T>> result = new TreeSet<>();
-               instances: for (Object instance : components.keySet()) {
-                       if (!clss.isAssignableFrom(instance.getClass()))
-                               continue instances;
-                       Component<? extends T> component = (Component<? extends T>) components.get(instance);
-
-                       if (component.isPublishedType(clss)) {
-                               if (filter != null) {
-                                       filter.test(component.getProperties());
-                               }
-                               result.add(component);
-                       }
-               }
-               if (result.isEmpty())
-                       return null;
-               return result;
-
-       }
-
-       synchronized long registerComponent(Component<?> component) {
-               if (started.get()) // TODO make it really dynamic
-                       throw new IllegalStateException("Already activated");
-               if (components.containsKey(component.get()))
-                       throw new IllegalArgumentException("Already registered as component");
-               components.put(component.get(), component);
-               return nextServiceId.incrementAndGet();
-       }
-
-       @Override
-       public synchronized Component<?> get(Object instance) {
-               if (!components.containsKey(instance))
-                       throw new IllegalArgumentException("Not registered as component");
-               return components.get(instance);
-       }
-
-       @Override
-       public synchronized 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 (InterruptedException e) {
-                       throw new RuntimeException("Register activation has been interrupted", e);
-               } catch (ExecutionException e) {
-                       if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) {
-                               throw (RuntimeException) e.getCause();
-                       } else {
-                               throw new IllegalStateException("Cannot activate register", e.getCause());
-                       }
-               }
-       }
-
-       @Override
-       public synchronized 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 (InterruptedException e) {
-                       throw new RuntimeException("Register deactivation has been interrupted", e);
-               } catch (ExecutionException e) {
-                       if (RuntimeException.class.isAssignableFrom(e.getCause().getClass())) {
-                               throw (RuntimeException) e.getCause();
-                       } else {
-                               throw new IllegalStateException("Cannot deactivate register", e.getCause());
-                       }
-               }
-       }
-
-       @Override
-       public synchronized boolean isActive() {
-               return started.get();
-       }
-
-       @Override
-       public synchronized void clear() {
-               components.clear();
-       }
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/AbstractWorkingCopy.java b/org.argeo.util/src/org/argeo/util/transaction/AbstractWorkingCopy.java
deleted file mode 100644 (file)
index 0da35ac..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.argeo.util.transaction;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public abstract class AbstractWorkingCopy<DATA, ATTR, ID> implements WorkingCopy<DATA, ATTR, ID> {
-       private Map<ID, DATA> newData = new HashMap<ID, DATA>();
-       private Map<ID, ATTR> modifiedData = new HashMap<ID, ATTR>();
-       private Map<ID, DATA> deletedData = new HashMap<ID, DATA>();
-
-       protected abstract ID getId(DATA data);
-
-       protected abstract ATTR cloneAttributes(DATA data);
-
-       public void cleanUp() {
-               // clean collections
-               newData.clear();
-               newData = null;
-               modifiedData.clear();
-               modifiedData = null;
-               deletedData.clear();
-               deletedData = null;
-       }
-
-       public boolean noModifications() {
-               return newData.size() == 0 && modifiedData.size() == 0 && deletedData.size() == 0;
-       }
-
-       public void startEditing(DATA user) {
-               ID id = getId(user);
-               if (modifiedData.containsKey(id))
-                       throw new IllegalStateException("Already editing " + id);
-               modifiedData.put(id, cloneAttributes(user));
-       }
-
-       public Map<ID, DATA> getNewData() {
-               return newData;
-       }
-
-       public Map<ID, DATA> getDeletedData() {
-               return deletedData;
-       }
-
-       public Map<ID, ATTR> getModifiedData() {
-               return modifiedData;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/JtaStatusAdapter.java b/org.argeo.util/src/org/argeo/util/transaction/JtaStatusAdapter.java
deleted file mode 100644 (file)
index bd97706..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-package org.argeo.util.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;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/SimpleRollbackException.java b/org.argeo.util/src/org/argeo/util/transaction/SimpleRollbackException.java
deleted file mode 100644 (file)
index 010b549..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.argeo.util.transaction;
-
-/** Internal unchecked rollback exception. */
-class SimpleRollbackException extends RuntimeException {
-       private static final long serialVersionUID = 8055601819719780566L;
-
-       public SimpleRollbackException() {
-               super();
-       }
-
-       public SimpleRollbackException(Throwable cause) {
-               super(cause);
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/SimpleTransaction.java b/org.argeo.util/src/org/argeo/util/transaction/SimpleTransaction.java
deleted file mode 100644 (file)
index 56ef063..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-package org.argeo.util.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;
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/SimpleTransactionManager.java b/org.argeo.util/src/org/argeo/util/transaction/SimpleTransactionManager.java
deleted file mode 100644 (file)
index f5be7c8..0000000
+++ /dev/null
@@ -1,214 +0,0 @@
-package org.argeo.util.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);
-//                     }
-//             }
-//
-//     }
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/TransactionStatusAdapter.java b/org.argeo.util/src/org/argeo/util/transaction/TransactionStatusAdapter.java
deleted file mode 100644 (file)
index a74fef1..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.argeo.util.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();
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/UuidXid.java b/org.argeo.util/src/org/argeo/util/transaction/UuidXid.java
deleted file mode 100644 (file)
index b6acebe..0000000
+++ /dev/null
@@ -1,132 +0,0 @@
-package org.argeo.util.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);
-       // }
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkContext.java b/org.argeo.util/src/org/argeo/util/transaction/WorkContext.java
deleted file mode 100644 (file)
index e818b83..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.argeo.util.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);
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkControl.java b/org.argeo.util/src/org/argeo/util/transaction/WorkControl.java
deleted file mode 100644 (file)
index db0e475..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.argeo.util.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();
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkTransaction.java b/org.argeo.util/src/org/argeo/util/transaction/WorkTransaction.java
deleted file mode 100644 (file)
index 245ca41..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.argeo.util.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();
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkingCopy.java b/org.argeo.util/src/org/argeo/util/transaction/WorkingCopy.java
deleted file mode 100644 (file)
index 9dd3fc5..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.argeo.util.transaction;
-
-import java.util.Map;
-
-public interface WorkingCopy<DATA, ATTR, ID> {
-       void startEditing(DATA user);
-
-       boolean noModifications();
-
-       void cleanUp();
-
-       Map<ID, DATA> getNewData();
-
-       Map<ID, DATA> getDeletedData();
-
-       Map<ID, ATTR> getModifiedData();
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkingCopyProcessor.java b/org.argeo.util/src/org/argeo/util/transaction/WorkingCopyProcessor.java
deleted file mode 100644 (file)
index cdd6404..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-package org.argeo.util.transaction;
-
-public interface WorkingCopyProcessor<WC extends WorkingCopy<?, ?, ?>> {
-       void prepare(WC wc);
-
-       void commit(WC wc);
-
-       void rollback(WC wc);
-       
-       WC newWorkingCopy();
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/WorkingCopyXaResource.java b/org.argeo.util/src/org/argeo/util/transaction/WorkingCopyXaResource.java
deleted file mode 100644 (file)
index ddb605a..0000000
+++ /dev/null
@@ -1,138 +0,0 @@
-package org.argeo.util.transaction;
-
-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. */
-public class WorkingCopyXaResource<WC extends WorkingCopy<?, ?, ?>> implements XAResource {
-       private final WorkingCopyProcessor<WC> processor;
-
-       private Map<Xid, WC> workingCopies = new HashMap<Xid, WC>();
-       private Xid editingXid = null;
-       private int transactionTimeout = 0;
-
-       public WorkingCopyXaResource(WorkingCopyProcessor<WC> processor) {
-               this.processor = processor;
-       }
-
-       @Override
-       public synchronized void start(Xid xid, int flags) throws XAException {
-               if (editingXid != null)
-                       throw new IllegalStateException("Already editing " + editingXid);
-               WC wc = workingCopies.put(xid, processor.newWorkingCopy());
-               if (wc != null)
-                       throw new IllegalStateException("There is already a working copy for " + xid);
-               this.editingXid = xid;
-       }
-
-       @Override
-       public void end(Xid xid, int flags) throws XAException {
-               checkXid(xid);
-       }
-
-       private WC wc(Xid xid) {
-               return workingCopies.get(xid);
-       }
-
-       public synchronized WC wc() {
-               if (editingXid == null)
-                       return null;
-               WC wc = workingCopies.get(editingXid);
-               if (wc == null)
-                       throw new IllegalStateException("No working copy found for " + editingXid);
-               return wc;
-       }
-
-       private synchronized void cleanUp(Xid xid) {
-               WC wc = workingCopies.get(xid);
-               if (wc != null) {
-                       wc.cleanUp();
-                       workingCopies.remove(xid);
-               }
-               editingXid = null;
-       }
-
-       @Override
-       public int prepare(Xid xid) throws XAException {
-               checkXid(xid);
-               WC wc = wc(xid);
-               if (wc.noModifications())
-                       return XA_RDONLY;
-               try {
-                       processor.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);
-                       WC wc = wc(xid);
-                       if (wc.noModifications())
-                               return;
-                       if (onePhase)
-                               processor.prepare(wc);
-                       processor.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);
-                       processor.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);
-       }
-
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/XAResourceProvider.java b/org.argeo.util/src/org/argeo/util/transaction/XAResourceProvider.java
deleted file mode 100644 (file)
index b0b211b..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.argeo.util.transaction;
-
-import javax.transaction.xa.XAResource;
-
-public interface XAResourceProvider {
-       XAResource getXaResource();
-}
diff --git a/org.argeo.util/src/org/argeo/util/transaction/package-info.java b/org.argeo.util/src/org/argeo/util/transaction/package-info.java
deleted file mode 100644 (file)
index f481161..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/** Minimalistic and partial XA transaction manager implementation. */
-package org.argeo.util.transaction;
\ No newline at end of file
index 50be8b7a79cd9d2da2179c9ae3acd4f849de9eaf..2cd600152d86674a6bcb84ed2fbd88d8e2bb140b 100644 (file)
@@ -14,9 +14,9 @@ import org.argeo.api.cms.CmsConstants;
 import org.argeo.api.cms.CmsLog;
 import org.argeo.api.cms.CmsState;
 import org.argeo.cms.CmsDeployProperty;
+import org.argeo.cms.util.LangUtils;
 import org.argeo.cms.websocket.server.CmsWebSocketConfigurator;
 import org.argeo.cms.websocket.server.TestEndpoint;
-import org.argeo.util.LangUtils;
 import org.eclipse.equinox.http.jetty.JettyConfigurator;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
index 393aede8fc3e92caf78eaaf5e41e0b6652f35ef6..dd761267d5e6d3097a88c54fea07c69a15b3cd4d 100644 (file)
@@ -12,11 +12,11 @@ import javax.inject.Inject;
 import javax.naming.InvalidNameException;
 import javax.naming.ldap.LdapName;
 
+import org.argeo.api.cms.keyring.CryptoKeyring;
+import org.argeo.api.cms.transaction.WorkTransaction;
 import org.argeo.cms.auth.CurrentUser;
-import org.argeo.cms.security.CryptoKeyring;
 import org.argeo.cms.swt.dialogs.CmsFeedback;
 import org.argeo.cms.swt.dialogs.CmsMessageDialog;
-import org.argeo.util.transaction.WorkTransaction;
 import org.eclipse.e4.core.di.annotations.Execute;
 import org.eclipse.e4.core.di.annotations.Optional;
 import org.eclipse.jface.dialogs.Dialog;
index f77c94d7c03eb994844b09015df240e4809569eb..91070f22286617a673ef525735e96943548b5e24 100644 (file)
@@ -3,7 +3,7 @@ package org.argeo.cms.e4.handlers;
 import javax.security.auth.Subject;
 
 import org.argeo.cms.auth.CurrentUser;
-import org.argeo.util.CurrentSubject;
+import org.argeo.cms.util.CurrentSubject;
 import org.eclipse.e4.core.di.annotations.Execute;
 import org.eclipse.e4.ui.workbench.IWorkbench;
 
index ddb6e1b33b0c12271d92d3ef573075ff11d45085..59624f93c48d364e554fd2042fad318e9d8168cb 100644 (file)
@@ -18,7 +18,7 @@ import org.argeo.api.cms.ux.CmsUi;
 import org.argeo.api.cms.ux.CmsView;
 import org.argeo.api.cms.ux.UxContext;
 import org.argeo.cms.auth.CurrentUser;
-import org.argeo.util.CurrentSubject;
+import org.argeo.cms.util.CurrentSubject;
 import org.eclipse.swt.widgets.Display;
 
 public abstract class AbstractSwtCmsView implements CmsView {
index 31555d168d10b1c2f8f1a44984014f20c458230a..67fa5ceac2b37efd000f18e5032ea6e2e63f948b 100644 (file)
@@ -12,7 +12,7 @@ import org.argeo.api.cms.CmsLog;
 import org.argeo.api.cms.ux.CmsTheme;
 import org.argeo.api.cms.ux.CmsView;
 import org.argeo.cms.swt.CmsSwtUtils;
-import org.argeo.util.LangUtils;
+import org.argeo.cms.util.LangUtils;
 import org.eclipse.rap.rwt.RWT;
 import org.eclipse.rap.rwt.application.Application;
 import org.eclipse.rap.rwt.application.Application.OperationMode;
index e94825db818c6014f30ea28ebf7be8ff42833884..a83a54db3926cfa2503079c22d3224dff28a8591 100644 (file)
@@ -5,7 +5,7 @@ import java.lang.System.Logger.Level;
 import java.nio.file.Path;
 
 import org.argeo.api.cms.CmsApp;
-import org.argeo.util.OS;
+import org.argeo.cms.util.OS;
 import org.eclipse.swt.events.DisposeListener;
 import org.eclipse.swt.widgets.Display;
 
index ac4217184138ec58c570892e2d414d35b72167c2..47ff35dc0e13ac6a6ac70f8482332c80445b873f 100644 (file)
@@ -7,7 +7,7 @@ import java.util.Collections;
 import java.util.Map;
 import java.util.TreeMap;
 
-import org.argeo.util.StreamUtils;
+import org.argeo.cms.util.StreamUtils;
 import org.eclipse.rap.rwt.service.ResourceManager;
 
 public class RcpResourceManager implements ResourceManager {