Merge branch 'master' of https://github.com/argeo/argeo-commons.git
authorMathieu Baudier <mbaudier@argeo.org>
Tue, 20 Nov 2018 14:10:50 +0000 (15:10 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Tue, 20 Nov 2018 14:10:50 +0000 (15:10 +0100)
1132 files changed:
.gitignore [new file with mode: 0644]
.project [new file with mode: 0644]
demo/.gitignore [new file with mode: 0644]
demo/all.policy [new file with mode: 0644]
demo/argeo_node.js [new file with mode: 0755]
demo/argeo_node_cli.properties [new file with mode: 0644]
demo/argeo_node_cluster_0.properties [new file with mode: 0644]
demo/argeo_node_cluster_1.properties [new file with mode: 0644]
demo/argeo_node_cms.properties [new file with mode: 0644]
demo/argeo_node_local.properties [new file with mode: 0644]
demo/argeo_node_osgiboot.properties [new file with mode: 0644]
demo/argeo_node_rap.properties [new file with mode: 0644]
demo/cms-e4-rap.properties [new file with mode: 0644]
demo/init/node/.gitignore [new file with mode: 0644]
demo/log4j.properties [new file with mode: 0644]
demo/pom.xml [new file with mode: 0644]
demo/ssl/.gitignore [new file with mode: 0644]
demo/ssl/openssl.cnf [new file with mode: 0644]
demo/ssl/openssl_root.cnf [new file with mode: 0644]
demo/ssl/ssl.sh [new file with mode: 0644]
dep/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.client/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.client/META-INF/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.client/bnd.bnd [new file with mode: 0644]
dep/org.argeo.dep.cms.client/build.properties [new file with mode: 0644]
dep/org.argeo.dep.cms.client/p2.inf [new file with mode: 0644]
dep/org.argeo.dep.cms.client/pom.xml [new file with mode: 0644]
dep/org.argeo.dep.cms.e4.rap/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.e4.rap/META-INF/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.e4.rap/bnd.bnd [new file with mode: 0644]
dep/org.argeo.dep.cms.e4.rap/build.properties [new file with mode: 0644]
dep/org.argeo.dep.cms.e4.rap/p2.inf [new file with mode: 0644]
dep/org.argeo.dep.cms.e4.rap/pom.xml [new file with mode: 0644]
dep/org.argeo.dep.cms.node/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.node/META-INF/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.node/bnd.bnd [new file with mode: 0644]
dep/org.argeo.dep.cms.node/build.properties [new file with mode: 0644]
dep/org.argeo.dep.cms.node/p2.inf [new file with mode: 0644]
dep/org.argeo.dep.cms.node/pom.xml [new file with mode: 0644]
dep/org.argeo.dep.cms.platform/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.platform/META-INF/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.platform/bnd.bnd [new file with mode: 0644]
dep/org.argeo.dep.cms.platform/build.properties [new file with mode: 0644]
dep/org.argeo.dep.cms.platform/p2.inf [new file with mode: 0644]
dep/org.argeo.dep.cms.platform/pom.xml [new file with mode: 0644]
dep/org.argeo.dep.cms.sdk/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.sdk/META-INF/.gitignore [new file with mode: 0644]
dep/org.argeo.dep.cms.sdk/bnd.bnd [new file with mode: 0644]
dep/org.argeo.dep.cms.sdk/build.properties [new file with mode: 0644]
dep/org.argeo.dep.cms.sdk/p2.inf [new file with mode: 0644]
dep/org.argeo.dep.cms.sdk/pom.xml [new file with mode: 0644]
dep/pom.xml [new file with mode: 0644]
dist/.gitignore [new file with mode: 0644]
dist/argeo-node/.gitignore [new file with mode: 0644]
dist/argeo-node/assembly/cms-e4-rap.xml [new file with mode: 0644]
dist/argeo-node/base/bin/argeo-cms [new file with mode: 0755]
dist/argeo-node/base/bin/argeo-cms.js [new file with mode: 0644]
dist/argeo-node/base/etc/argeo/argeo.ini [new file with mode: 0644]
dist/argeo-node/base/etc/argeo/conf.d/app-template.txt [new file with mode: 0644]
dist/argeo-node/base/etc/argeo/log4j.properties [new file with mode: 0644]
dist/argeo-node/base/etc/argeo/settings.sh [new file with mode: 0644]
dist/argeo-node/base/share/argeo/SETUP.txt [new file with mode: 0644]
dist/argeo-node/base/share/argeo/all.policy [new file with mode: 0644]
dist/argeo-node/base/share/argeo/argeo-pgsql-setup.sql [new file with mode: 0644]
dist/argeo-node/base/share/argeo/argeo-slapd-setup.inf [new file with mode: 0644]
dist/argeo-node/base/share/argeo/cms.js [new file with mode: 0755]
dist/argeo-node/base/share/argeo/config.ini [new file with mode: 0644]
dist/argeo-node/pom.xml [new file with mode: 0644]
dist/argeo-node/rpm/etc/node/conf.d/app-template.txt [new file with mode: 0644]
dist/argeo-node/rpm/etc/node/log4j.properties [new file with mode: 0644]
dist/argeo-node/rpm/etc/node/node.ini [new file with mode: 0644]
dist/argeo-node/rpm/etc/node/settings.sh [new file with mode: 0644]
dist/argeo-node/rpm/scripts/preinstall [new file with mode: 0644]
dist/argeo-node/rpm/usr/lib/systemd/system/argeo.service [new file with mode: 0644]
dist/argeo-node/rpm/usr/sbin/argeoctl [new file with mode: 0755]
dist/argeo-node/rpm/usr/share/node/all.policy [new file with mode: 0644]
dist/argeo-node/rpm/usr/share/node/config.ini [new file with mode: 0644]
dist/argeo-node/rpm/usr/share/node/jjs/cms.js [new file with mode: 0755]
dist/osgi-boot/.gitignore [new file with mode: 0644]
dist/osgi-boot/assembly/osgi-boot.xml [new file with mode: 0644]
dist/osgi-boot/base/bin/a2jjs [new file with mode: 0755]
dist/osgi-boot/pom.xml [new file with mode: 0644]
dist/osgi-boot/src/main/rpm/etc/osgiboot/all.policy [new file with mode: 0644]
dist/osgi-boot/src/main/rpm/etc/osgiboot/osgi-service-init-functions.sh [new file with mode: 0644]
dist/osgi-boot/src/main/rpm/etc/osgiboot/osgi-service-settings.sh [new file with mode: 0644]
dist/osgi-boot/src/main/rpm/usr/bin/a2jjs [new file with mode: 0755]
dist/osgi-boot/src/main/rpm/usr/sbin/osgi-service [new file with mode: 0644]
dist/pom.xml [new file with mode: 0644]
doc/.gitignore [new file with mode: 0644]
doc/docbook/argeo-commons.dbk.xml [new file with mode: 0644]
doc/docbook/argeo.css [new file with mode: 0644]
doc/docbook/deploying.dbk.xml [new file with mode: 0644]
doc/docbook/overview.dbk.xml [new file with mode: 0644]
doc/pom.xml [new file with mode: 0644]
license-apache2-header.txt [new file with mode: 0644]
maven/.gitignore [new file with mode: 0644]
maven/assembly-descriptors/.gitignore [new file with mode: 0644]
maven/assembly-descriptors/META-INF/.gitignore [new file with mode: 0644]
maven/assembly-descriptors/assemblies/a2-source-tp.xml [new file with mode: 0644]
maven/assembly-descriptors/assemblies/a2-source.xml [new file with mode: 0644]
maven/assembly-descriptors/bnd.bnd [new file with mode: 0644]
maven/assembly-descriptors/pom.xml [new file with mode: 0644]
maven/pom.xml [new file with mode: 0644]
org.argeo.cms.e4.rap/.classpath [new file with mode: 0644]
org.argeo.cms.e4.rap/.gitignore [new file with mode: 0644]
org.argeo.cms.e4.rap/.project [new file with mode: 0644]
org.argeo.cms.e4.rap/META-INF/.gitignore [new file with mode: 0644]
org.argeo.cms.e4.rap/OSGI-INF/cms-admin-rap.xml [new file with mode: 0644]
org.argeo.cms.e4.rap/OSGI-INF/cms-demo-rap.xml [new file with mode: 0644]
org.argeo.cms.e4.rap/bnd.bnd [new file with mode: 0644]
org.argeo.cms.e4.rap/build.properties [new file with mode: 0644]
org.argeo.cms.e4.rap/cms/app.js [new file with mode: 0644]
org.argeo.cms.e4.rap/e4xmi/cms-demo-rap.e4xmi [new file with mode: 0644]
org.argeo.cms.e4.rap/pom.xml [new file with mode: 0644]
org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/AbstractRapE4App.java [new file with mode: 0644]
org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4AdminApp.java [new file with mode: 0644]
org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4DemoApp.java [new file with mode: 0644]
org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4EntryPointFactory.java [new file with mode: 0644]
org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsLoginLifecycle.java [new file with mode: 0644]
org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/CkEditor-removeButtons.js [new file with mode: 0644]
org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/CkEditor-toolbar.js [new file with mode: 0644]
org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/CkEditor-toolbarGroups.json [new file with mode: 0644]
org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/HtmlEditor.java [new file with mode: 0644]
org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/test.json [new file with mode: 0644]
org.argeo.cms.e4/.classpath [new file with mode: 0644]
org.argeo.cms.e4/.gitignore [new file with mode: 0644]
org.argeo.cms.e4/.project [new file with mode: 0644]
org.argeo.cms.e4/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
org.argeo.cms.e4/META-INF/.gitignore [new file with mode: 0644]
org.argeo.cms.e4/OSGI-INF/defaultCallbackHandler.xml [new file with mode: 0644]
org.argeo.cms.e4/OSGI-INF/homeRepository.xml [new file with mode: 0644]
org.argeo.cms.e4/OSGI-INF/userAdminWrapper.xml [new file with mode: 0644]
org.argeo.cms.e4/bnd.bnd [new file with mode: 0644]
org.argeo.cms.e4/build.properties [new file with mode: 0644]
org.argeo.cms.e4/e4xmi/cms-devops.e4xmi [new file with mode: 0644]
org.argeo.cms.e4/pom.xml [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/CmsE4Utils.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/PrivilegedJob.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/addons/AuthAddon.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/addons/LocaleAddon.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/contexts/OsgiFilterContextFunction.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/files/NodeFsBrowserView.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangeLanguage.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangePassword.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseAllParts.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseWorkbench.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/DoNothing.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/LanguageMenuContribution.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/OpenPerspective.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/SaveAllParts.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/SavePart.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/GenericPropertyPage.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/JcrBrowserView.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/JcrE4DClickListener.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/JcrNodeEditor.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/SimplePart.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/AddFolderNode.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/AddRemoteRepository.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/DeleteNodes.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/Refresh.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/RenameNode.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/OsgiConfigurationsView.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/parts/CmsDocBookEditor.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/parts/CmsTextEditor.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/sys/handlers/ShowDesktop.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/sys/handlers/Shutdown.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/AbstractRoleEditor.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/CmsWorkbenchStyles.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/GroupEditor.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/GroupsView.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/SecurityAdminImages.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/UiAdminUtils.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/UiUserAdminListener.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserBatchUpdateWizard.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserEditor.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserTableDefaultDClickListener.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/UsersView.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/DeleteGroups.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/DeleteUsers.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/CommonNameLP.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/DomainNameLP.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/MailLP.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/RoleIconLP.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserAdminAbstractLP.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserDragListener.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserFilter.java [new file with mode: 0644]
org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserNameLP.java [new file with mode: 0644]
org.argeo.cms.ui.theme/.classpath [new file with mode: 0644]
org.argeo.cms.ui.theme/.gitignore [new file with mode: 0644]
org.argeo.cms.ui.theme/.project [new file with mode: 0644]
org.argeo.cms.ui.theme/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
org.argeo.cms.ui.theme/.settings/org.eclipse.pde.core.prefs [new file with mode: 0644]
org.argeo.cms.ui.theme/META-INF/.gitignore [new file with mode: 0644]
org.argeo.cms.ui.theme/bnd.bnd [new file with mode: 0644]
org.argeo.cms.ui.theme/build.properties [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/active.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/add.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/add.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/addFolder.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/addPrivileges.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/addRepo.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/addWorkspace.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/adminLog.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/batch.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/begin.gif [new file with mode: 0755]
org.argeo.cms.ui.theme/icons/binary.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/browser.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/bundles.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/changePassword.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/clear.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/close-all.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/commit.gif [new file with mode: 0755]
org.argeo.cms.ui.theme/icons/delete.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/dumpNode.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/file.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/folder.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/getSize.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/group.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/home.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/home.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/import_fs.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/installed.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/log.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/logout.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/maintenance.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/node.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/nodes.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/osgi_explorer.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/password.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/person-logged-in.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/person.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/query.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/refresh.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/remote_connected.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/remote_disconnected.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/remove.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/removePrivileges.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/rename.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/repositories.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/repository_connected.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/repository_disconnected.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/resolved.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/role.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/rollback.gif [new file with mode: 0755]
org.argeo.cms.ui.theme/icons/save-all.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/save.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/save.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/save_security.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/save_security_disabled.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/security.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/service_published.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/service_referenced.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/sort.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/starting.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/sync.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/user.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/users.gif [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/workgroup.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/workgroup.xcf [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/workspace_connected.png [new file with mode: 0644]
org.argeo.cms.ui.theme/icons/workspace_disconnected.png [new file with mode: 0644]
org.argeo.cms.ui.theme/pom.xml [new file with mode: 0644]
org.argeo.cms.ui.theme/rap/argeo-studio.css [new file with mode: 0644]
org.argeo.cms.ui.theme/src/org/argeo/cms/ui/theme/CmsImages.java [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/.classpath [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/.gitignore [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/.project [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/META-INF/.gitignore [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/META-INF/spring/commands.xml [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/META-INF/spring/osgi.xml [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle.properties [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle_de.properties [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle_fr.properties [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle_ru.properties [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/bnd.bnd [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/branding/afterLogout.html [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/branding/empty.html [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/branding/favicon.ico [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/branding/login.html [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/branding/public.html [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/build.properties [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/active.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/add.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/addFolder.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/addPrivileges.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/addRepo.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/addWorkspace.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/binary.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/browser.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/bundles.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/close-all.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/closeAll.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/dumpNode.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/exit.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/file.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/folder.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/getSize.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/home.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/home.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/import_fs.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/installed.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/main.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/node.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/nodes.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/osgi_explorer.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/password.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/person-logged-in.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/preferences.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/query.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/refresh.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/remote_connected.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/remote_disconnected.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/remove.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/removePrivileges.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/rename.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/repositories.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/repository_connected.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/repository_disconnected.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/resolved.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/role.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/save-all.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/save.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/security.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/service_published.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/service_referenced.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/sort.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/starting.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/user.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/users.gif [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/workspace_connected.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/icons/workspace_disconnected.png [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/plugin.xml [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/pom.xml [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/AnonymousEntryPoint.java [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapActionBarAdvisor.java [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWindowAdvisor.java [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWorkbenchAdvisor.java [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWorkbenchLogin.java [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/SecureRapActivator.java [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/SpnegoWorkbenchLogin.java [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/commands/OpenHome.java [new file with mode: 0644]
org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/commands/UserMenu.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/.classpath [new file with mode: 0644]
org.argeo.cms.ui.workbench/.gitignore [new file with mode: 0644]
org.argeo.cms.ui.workbench/.project [new file with mode: 0644]
org.argeo.cms.ui.workbench/META-INF/.gitignore [new file with mode: 0644]
org.argeo.cms.ui.workbench/META-INF/spring/commands.xml [new file with mode: 0644]
org.argeo.cms.ui.workbench/META-INF/spring/common.xml [new file with mode: 0644]
org.argeo.cms.ui.workbench/META-INF/spring/osgi.xml [new file with mode: 0644]
org.argeo.cms.ui.workbench/META-INF/spring/parts.xml [new file with mode: 0644]
org.argeo.cms.ui.workbench/OSGI-INF/l10n/bundle.properties [new file with mode: 0644]
org.argeo.cms.ui.workbench/OSGI-INF/l10n/bundle_de.properties [new file with mode: 0644]
org.argeo.cms.ui.workbench/bnd.bnd [new file with mode: 0644]
org.argeo.cms.ui.workbench/build.properties [new file with mode: 0644]
org.argeo.cms.ui.workbench/keyring.properties [new file with mode: 0644]
org.argeo.cms.ui.workbench/plugin.xml [new file with mode: 0644]
org.argeo.cms.ui.workbench/pom.xml [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/CmsWorkbenchStyles.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/JcrBrowserPerspective.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/MaintenancePerspective.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/OsgiExplorerPerspective.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/SecurityAdminPerspective.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/UserHomePerspective.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/WorkbenchUiPlugin.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/DoNothing.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenChangePasswordDialog.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenEditor.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenHomePerspective.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/WorkbenchConstants.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/AddFolderNode.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/AddPrivileges.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/AddRemoteRepository.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/ConfigurableNodeDump.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/CreateWorkspace.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DeleteNodes.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DumpNode.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/EditNode.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/GetNodeSize.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/Refresh.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/RemovePrivileges.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/RemoveRemoteRepository.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/RenameNode.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/SortChildNodes.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/UploadFiles.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/AbstractJcrQueryEditor.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/AddPrivilegeWizard.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/ChildNodesPage.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/ChooseNameDialog.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/GenericNodeEditorInput.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/GenericNodePage.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/GenericPropertyPage.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/JcrQueryEditorInput.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/NodeEditorInput.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/NodePrivilegesPage.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/NodeVersionHistoryPage.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/StringNodeEditorInput.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/PartStateChanged.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/SecurityAdminImages.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/UiAdminUtils.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/UiUserAdminListener.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/UserAdminWrapper.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/DeleteGroups.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/DeleteUsers.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/ForceRefresh.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/NewGroup.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/NewUser.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/SaveArgeoUser.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/UserBatchUpdate.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/UserTransactionHandler.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/ArgeoUserEditorInput.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/GroupMainPage.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/GroupsView.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserBatchUpdateWizard.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserEditor.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserEditorInput.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserMainPage.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UsersView.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/CommonNameLP.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/DomainNameLP.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/MailLP.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/RoleIconLP.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserAdminAbstractLP.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserDragListener.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserFilter.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserNameLP.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserTableDefaultDClickListener.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserTransactionProvider.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/DefaultNodeEditor.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/GenericJcrQueryEditor.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/JcrBrowserView.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/NodeFsBrowserView.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/WorkbenchJcrDClickListener.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/messages.properties [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/BundleNode.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/BundlesView.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/CmsSessionsView.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/ModulesView.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/MultiplePackagesView.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/OsgiExplorerImages.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/ServiceReferenceNode.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/StateLabelProvider.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/AdminLogView.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/LogContentProvider.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/LogView.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/UserProfile.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/util/CommandUtils.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/util/PrivilegedJob.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/util/RolesSourceProvider.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/eclipse/spring/ApplicationContextTracker.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/eclipse/spring/SpringCommandHandler.java [new file with mode: 0644]
org.argeo.cms.ui.workbench/src/org/argeo/eclipse/spring/SpringExtensionFactory.java [new file with mode: 0644]
org.argeo.cms.ui/.classpath [new file with mode: 0644]
org.argeo.cms.ui/.gitignore [new file with mode: 0644]
org.argeo.cms.ui/.project [new file with mode: 0644]
org.argeo.cms.ui/META-INF/.gitignore [new file with mode: 0644]
org.argeo.cms.ui/bnd.bnd [new file with mode: 0644]
org.argeo.cms.ui/build.properties [new file with mode: 0644]
org.argeo.cms.ui/icons/loading.gif [new file with mode: 0644]
org.argeo.cms.ui/icons/noPic-goldenRatio-640px.png [new file with mode: 0644]
org.argeo.cms.ui/icons/noPic-square-640px.png [new file with mode: 0644]
org.argeo.cms.ui/pom.xml [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/forms/EditableLink.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/forms/EditableMultiStringProperty.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/forms/EditablePropertyDate.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/forms/EditablePropertyString.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/forms/FormConstants.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/forms/FormEditorHeader.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/forms/FormPageViewer.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/forms/FormStyle.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/forms/FormUtils.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/maintenance/AbstractOsgiComposite.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/maintenance/Browse.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/maintenance/ConnectivityDeploymentUi.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/maintenance/DataDeploymentUi.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/maintenance/DeploymentEntryPoint.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/maintenance/LogDeploymentUi.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/maintenance/MaintenanceStyles.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/maintenance/MaintenanceUi.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/maintenance/NonAdminPage.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/maintenance/SecurityDeploymentUi.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/script/AppUi.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/script/Branding.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/script/CmsScriptApp.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/script/CmsScriptRwtApplication.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/script/ScriptAppActivator.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/script/ScriptUi.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/script/Theme.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/script/cms.js [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/CustomTextEditor.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/DbkTextInterpreter.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/DocumentPage.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/DocumentTextEditor.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/IdentityTextInterpreter.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/Img.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/Paragraph.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/StandardTextEditor.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/TextEditorHeader.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/TextInterpreter.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/TextSection.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/TextStyles.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/text/WikiPage.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/CmsConstants.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/CmsEditable.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/CmsEditionEvent.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/CmsImageManager.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/CmsStyles.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/CmsUiProvider.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/CmsView.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/LifeCycleUiProvider.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/UxContext.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsFeedback.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsMessageDialog.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsWizardDialog.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormColors.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/fs/CmsFsBrowser.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/fs/FsContextMenu.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/fs/FsStyles.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/Activator.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/ImageManagerImpl.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/JcrContentProvider.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/JcrFileUploadReceiver.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/SimpleEditableImage.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/rwt/UserUi.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/AbstractDbkViewer.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/AbstractTextViewer.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/DbkContextMenu.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/MarkupValidatorCopy.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/SectionTitle.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/TextContextMenu.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/TextInterpreterImpl.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrDClickListener.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrImages.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/NodeContentProvider.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/RepositoryRegister.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/useradmin/PickUpUserDialog.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/useradmin/UserLP.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/ui/useradmin/UsersImages.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/BundleResourceLoader.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/CmsLink.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/CmsPane.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/CmsUtils.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/LoginEntryPoint.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/MenuLink.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/SimpleApp.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/SimpleCmsHeader.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/SimpleDynamicPages.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/SimpleErgonomics.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/SimpleImageManager.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/SimpleStaticPage.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/SimpleUxContext.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/StyleSheetResourceLoader.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/SystemNotifications.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/ThemeUtils.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/UserAdminUtils.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/UserMenu.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/UserMenuLink.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/util/VerticalMenu.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/viewers/AbstractPageViewer.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/viewers/EditablePart.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/viewers/ItemPart.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/viewers/JcrVersionCmsEditable.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/viewers/NodePart.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/viewers/PropertyPart.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/viewers/Section.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/viewers/SectionPart.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/EditableImage.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/EditableText.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/JcrComposite.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/ScrolledPage.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/StyledControl.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/AbstractLoginDialog.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CmsLogin.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CmsLoginShell.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CompositeCallbackHandler.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/DefaultLoginDialog.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/DynamicCallbackHandler.java [new file with mode: 0644]
org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/LocaleChoice.java [new file with mode: 0644]
org.argeo.cms/.classpath [new file with mode: 0644]
org.argeo.cms/.gitignore [new file with mode: 0644]
org.argeo.cms/.project [new file with mode: 0644]
org.argeo.cms/META-INF/.gitignore [new file with mode: 0644]
org.argeo.cms/OSGI-INF/l10n/bundle.properties [new file with mode: 0644]
org.argeo.cms/OSGI-INF/l10n/bundle_ar.properties [new file with mode: 0644]
org.argeo.cms/OSGI-INF/l10n/bundle_de.properties [new file with mode: 0644]
org.argeo.cms/OSGI-INF/l10n/bundle_fr.properties [new file with mode: 0644]
org.argeo.cms/OSGI-INF/l10n/bundle_ru.properties [new file with mode: 0644]
org.argeo.cms/bnd.bnd [new file with mode: 0644]
org.argeo.cms/build.properties [new file with mode: 0644]
org.argeo.cms/ext/test/org/argeo/cms/security/PasswordBasedEncryptionTest.java [new file with mode: 0644]
org.argeo.cms/ext/test/org/argeo/cms/security/RunHttpSpnego.java [new file with mode: 0644]
org.argeo.cms/ext/test/org/argeo/cms/tabular/JcrTabularTest.java [new file with mode: 0644]
org.argeo.cms/pom.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/ArgeoNames.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/ArgeoTypes.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsException.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsMsg.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsNames.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/CmsTypes.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/CmsSessionId.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/DataAdminLoginModule.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/HttpRequestCallback.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/HttpRequestCallbackHandler.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/SpnegoLoginModule.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/cmd/Sync.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/cmd/SyncFileVisitor.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/cms.cnd [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/i18n/DefaultsResourceBundle.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/i18n/LocaleUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/i18n/Localized.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/auth/ConsoleCallbackHandler.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/auth/ImpliedByPrincipal.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/DataHttpContext.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/HttpConstants.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/HttpUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/LinkServlet.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/PrivateHttpContext.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/RobotServlet.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/WebCmsSessionImpl.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/client/HttpCredentialProvider.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoAuthScheme.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoCredentials.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/client/jaas.cfg [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/protectedHandlers.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/http/webdav-config.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/jcr/JackrabbitType.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/jcr/RepoConf.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/jcr/RepositoryBuilder.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-h2.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-localfs.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-memory.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster_ds.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql_ds.xml [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsDeployment.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsFsProvider.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsInstance.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsPaths.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsShutdown.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsState.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/DataModels.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/DeployConfig.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/GogoShellKiller.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/HomeRepository.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/InitUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelThread.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/LocalRepository.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeAuthorization.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttp.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeKeyRing.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeLogger.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeRepositoryFactory.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeUserAdmin.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/PkiUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryServiceFactory.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/SecurityProfile.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/dc=example,dc=com.ldif [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas-ipa.cfg [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas.cfg [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/ou=roles,ou=node.ldif [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/internal/kernel/ou=tokens,ou=node.ldif [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/security/ChecksumFactory.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/security/JcrKeyring.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/spring/AbstractSystemExecution.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/spring/AuthenticatedApplicationContextInitialization.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/spring/SimpleRoleRegistration.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/spring/osgi/OsgiModuleLabel.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/tabular/CsvTabularWriter.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/tabular/JcrTabularRowIterator.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/tabular/JcrTabularWriter.java [new file with mode: 0644]
org.argeo.eclipse.ui.rap/.classpath [new file with mode: 0644]
org.argeo.eclipse.ui.rap/.gitignore [new file with mode: 0644]
org.argeo.eclipse.ui.rap/.project [new file with mode: 0644]
org.argeo.eclipse.ui.rap/META-INF/.gitignore [new file with mode: 0644]
org.argeo.eclipse.ui.rap/bnd.bnd [new file with mode: 0644]
org.argeo.eclipse.ui.rap/build.properties [new file with mode: 0644]
org.argeo.eclipse.ui.rap/pom.xml [new file with mode: 0644]
org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/EclipseUiSpecificUtils.java [new file with mode: 0644]
org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/OpenFile.java [new file with mode: 0644]
org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/OpenFileService.java [new file with mode: 0644]
org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/SingleSourcingException.java [new file with mode: 0644]
org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/UiContext.java [new file with mode: 0644]
org.argeo.eclipse.ui/.classpath [new file with mode: 0644]
org.argeo.eclipse.ui/.gitignore [new file with mode: 0644]
org.argeo.eclipse.ui/.project [new file with mode: 0644]
org.argeo.eclipse.ui/META-INF/.gitignore [new file with mode: 0644]
org.argeo.eclipse.ui/bnd.bnd [new file with mode: 0644]
org.argeo.eclipse.ui/build.properties [new file with mode: 0644]
org.argeo.eclipse.ui/pom.xml [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/AbstractTreeContentProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/ColumnDefinition.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/ColumnViewerComparator.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/EclipseJcrMonitor.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/EclipseUiException.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/EclipseUiUtils.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/FileProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/GenericTableComparator.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/IListProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/Selected.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/TreeParent.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/ErrorFeedback.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/FeedbackDialog.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/LightweightDialog.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/NonModalErrorFeedback.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/SingleValue.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/AdvancedFsBrowser.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FileIconNameLabelProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsTableViewer.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsTreeViewer.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsUiConstants.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsUiException.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsUiUtils.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/NioFileLabelProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/ParentDir.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/SimpleFsBrowser.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/SimpleFsTreeBrowser.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/file.png [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/folder.png [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/AbstractNodeContentProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/DefaultNodeLabelProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/JcrUiUtils.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/NodeElementComparer.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/NodesWrapper.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/SimpleNodeContentProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/WrappedNode.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/JcrColumnDefinition.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/NodeViewerComparator.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/RowViewerComparator.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrNodeLabelProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrRowLabelProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/JcrFileProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/JcrItemsComparator.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/NodeViewerComparer.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/SingleSessionFileProvider.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/parts/LdifUsersTable.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/utils/SingleSourcingConstants.java [new file with mode: 0644]
org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/utils/ViewerUtils.java [new file with mode: 0644]
org.argeo.enterprise/.classpath [new file with mode: 0644]
org.argeo.enterprise/.gitignore [new file with mode: 0644]
org.argeo.enterprise/.project [new file with mode: 0644]
org.argeo.enterprise/META-INF/.gitignore [new file with mode: 0644]
org.argeo.enterprise/bnd.bnd [new file with mode: 0644]
org.argeo.enterprise/build.properties [new file with mode: 0644]
org.argeo.enterprise/ext/test/log4j.properties [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/BasicTestConstants.java [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/LdifParserTest.java [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/LdifUserAdminTest.java [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/UserAdminConfTest.java [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/basic.ldif [new file with mode: 0644]
org.argeo.enterprise/pom.xml [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/AttributesDictionary.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/AuthPassword.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/DnsBrowser.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.csv [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/LdapObjs.csv [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/LdapObjs.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/LdifParser.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/LdifWriter.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/SharedSecret.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/SpecifiedName.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/naming/SrvRecord.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumAD.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumOCD.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryGroup.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryUser.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/IpaUtils.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifAuthorization.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifGroup.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUserAdmin.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserDirectory.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserUtils.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectory.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryException.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryWorkingCopy.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/WcXaResource.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/osgi/useradmin/jaas-os.cfg [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/transaction/simple/SimpleTransaction.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/transaction/simple/SimpleTransactionException.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/transaction/simple/SimpleTransactionManager.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/transaction/simple/UuidXid.java [new file with mode: 0644]
org.argeo.ext.jackrabbit/.classpath [new file with mode: 0644]
org.argeo.ext.jackrabbit/.gitignore [new file with mode: 0644]
org.argeo.ext.jackrabbit/.project [new file with mode: 0644]
org.argeo.ext.jackrabbit/META-INF/.gitignore [new file with mode: 0644]
org.argeo.ext.jackrabbit/bnd.bnd [new file with mode: 0644]
org.argeo.ext.jackrabbit/build.properties [new file with mode: 0644]
org.argeo.ext.jackrabbit/ext/test/log4j.properties [new file with mode: 0644]
org.argeo.ext.jackrabbit/ext/test/org/argeo/security/jackrabbit/JackrabbitAuthTest.java [new file with mode: 0644]
org.argeo.ext.jackrabbit/ext/test/org/argeo/security/jackrabbit/repository-memory-test.xml [new file with mode: 0644]
org.argeo.ext.jackrabbit/pom.xml [new file with mode: 0644]
org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java [new file with mode: 0644]
org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoAccessManager.java [new file with mode: 0644]
org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java [new file with mode: 0644]
org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/SystemJackrabbitLoginModule.java [new file with mode: 0644]
org.argeo.ext.rap.ui.workbench/.gitignore [new file with mode: 0644]
org.argeo.ext.rap.ui.workbench/.project [new file with mode: 0644]
org.argeo.ext.rap.ui.workbench/META-INF/.gitignore [new file with mode: 0644]
org.argeo.ext.rap.ui.workbench/META-INF/spring/osgi.xml [new file with mode: 0644]
org.argeo.ext.rap.ui.workbench/bnd.bnd [new file with mode: 0644]
org.argeo.ext.rap.ui.workbench/build.properties [new file with mode: 0644]
org.argeo.ext.rap.ui.workbench/pom.xml [new file with mode: 0644]
org.argeo.jcr/.classpath [new file with mode: 0644]
org.argeo.jcr/.gitignore [new file with mode: 0644]
org.argeo.jcr/.project [new file with mode: 0644]
org.argeo.jcr/META-INF/.gitignore [new file with mode: 0644]
org.argeo.jcr/bnd.bnd [new file with mode: 0644]
org.argeo.jcr/build.properties [new file with mode: 0644]
org.argeo.jcr/ext/test/log4j.properties [new file with mode: 0644]
org.argeo.jcr/ext/test/org/argeo/jcr/docbook/DocBookModelTest.java [new file with mode: 0644]
org.argeo.jcr/ext/test/org/argeo/jcr/docbook/WikipediaSample.dbk.xml [new file with mode: 0644]
org.argeo.jcr/ext/test/org/argeo/jcr/docbook/howto.xml [new file with mode: 0644]
org.argeo.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java [new file with mode: 0644]
org.argeo.jcr/ext/test/org/argeo/server/jcr/JcrResourceAdapterTest.java [new file with mode: 0644]
org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy00.xls [new file with mode: 0644]
org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy01.xls [new file with mode: 0644]
org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy02.xls [new file with mode: 0644]
org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy03.xls [new file with mode: 0644]
org.argeo.jcr/pom.xml [new file with mode: 0644]
org.argeo.jcr/repository.xml [new file with mode: 0644]
org.argeo.jcr/repository/repository/meta/rootUUID [new file with mode: 0644]
org.argeo.jcr/repository/repository/namespaces/ns_idx.properties [new file with mode: 0644]
org.argeo.jcr/repository/repository/namespaces/ns_reg.properties [new file with mode: 0644]
org.argeo.jcr/repository/workspaces/default/workspace.xml [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/JackrabbitRepositoryFactory.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/repository-h2.xml [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/repository-localfs.xml [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/repository-memory.xml [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/unit/jaas.config [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/unit/repository-h2.xml [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jackrabbit/unit/repository-memory.xml [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/ArgeoJcrException.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/Bin.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/CollectionNodeIterator.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/DefaultJcrListener.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/JcrAuthorizations.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/JcrCallback.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/JcrMonitor.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/JcrResourceAdapter.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/PropertyDiff.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/SimplePrincipal.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/VersionDiff.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookModel.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookNames.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookTypes.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/docbook/docbook-full.cnd [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/docbook/docbook.cnd [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/BinaryChannel.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileStore.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrFsException.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/SessionFsProvider.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/Text.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/fs/WorkSpaceFileStore.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/spring/ThreadBoundSession.java [new file with mode: 0644]
org.argeo.jcr/src/org/argeo/jcr/unit/AbstractJcrTestCase.java [new file with mode: 0644]
org.argeo.maintenance/.classpath [new file with mode: 0644]
org.argeo.maintenance/.gitignore [new file with mode: 0644]
org.argeo.maintenance/.project [new file with mode: 0644]
org.argeo.maintenance/META-INF/.gitignore [new file with mode: 0644]
org.argeo.maintenance/bnd.bnd [new file with mode: 0644]
org.argeo.maintenance/build.properties [new file with mode: 0644]
org.argeo.maintenance/pom.xml [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/MaintenanceException.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/AbstractAtomicBackup.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/AtomicBackup.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupContext.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupFileSystemManager.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupPurge.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupUtils.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/MySqlBackup.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/OpenLdapBackup.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/OsCallBackup.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/PostgreSqlBackup.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SimpleBackupContext.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SimpleBackupPurge.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SvnBackup.java [new file with mode: 0644]
org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SystemBackup.java [new file with mode: 0644]
org.argeo.node.api/.classpath [new file with mode: 0644]
org.argeo.node.api/.gitignore [new file with mode: 0644]
org.argeo.node.api/.project [new file with mode: 0644]
org.argeo.node.api/META-INF/.gitignore [new file with mode: 0644]
org.argeo.node.api/bnd.bnd [new file with mode: 0644]
org.argeo.node.api/build.properties [new file with mode: 0644]
org.argeo.node.api/pom.xml [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/ArgeoLogListener.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/ArgeoLogger.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/DataModelNamespace.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/NodeConstants.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/NodeDeployment.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/NodeInstance.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/NodeNames.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/NodeOID.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/NodeState.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/NodeTypes.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/NodeUtils.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/node.cnd [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/package-info.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/packageinfo [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/security/AnonymousPrincipal.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/security/CryptoKeyring.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/security/DataAdminPrincipal.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/security/Keyring.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/security/NodeSecurityUtils.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/security/PBEKeySpecCallback.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/tabular/ArrayTabularRow.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/tabular/TabularColumn.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/tabular/TabularContent.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/tabular/TabularRow.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/tabular/TabularRowIterator.java [new file with mode: 0644]
org.argeo.node.api/src/org/argeo/node/tabular/TabularWriter.java [new file with mode: 0644]
org.argeo.osgi.boot/.classpath [new file with mode: 0644]
org.argeo.osgi.boot/.gitignore [new file with mode: 0644]
org.argeo.osgi.boot/.project [new file with mode: 0644]
org.argeo.osgi.boot/META-INF/.gitignore [new file with mode: 0644]
org.argeo.osgi.boot/bnd.bnd [new file with mode: 0644]
org.argeo.osgi.boot/build.properties [new file with mode: 0644]
org.argeo.osgi.boot/ext/test/org/argeo/osgi/boot/OsgiBootNoRuntimeTest.java [new file with mode: 0644]
org.argeo.osgi.boot/ext/test/org/argeo/osgi/boot/OsgiBootRuntimeTest.java [new file with mode: 0644]
org.argeo.osgi.boot/pom.xml [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/Activator.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/AdminThread.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/BundlesSet.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/DistributionBundle.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/Launcher.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/Main.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/NodeRunner.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBoot.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootConstants.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootDiagnostics.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootException.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootUtils.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBuilder.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Branch.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Component.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Contribution.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Module.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsA2Source.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsM2Source.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/OsgiContext.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningManager.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningSource.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/AntPathMatcher.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/CollectionUtils.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/ObjectUtils.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/PathMatcher.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/StringUtils.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/SystemPropertyUtils.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/log4j.properties [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/node.policy [new file with mode: 0644]
org.argeo.util/.classpath [new file with mode: 0644]
org.argeo.util/.gitignore [new file with mode: 0644]
org.argeo.util/.project [new file with mode: 0644]
org.argeo.util/META-INF/.gitignore [new file with mode: 0644]
org.argeo.util/bnd.bnd [new file with mode: 0644]
org.argeo.util/build.properties [new file with mode: 0644]
org.argeo.util/ext/test/org/argeo/util/CsvParserEncodingTestCase.java [new file with mode: 0644]
org.argeo.util/ext/test/org/argeo/util/CsvParserParseFileTest.java [new file with mode: 0644]
org.argeo.util/ext/test/org/argeo/util/CsvParserTestCase.java [new file with mode: 0644]
org.argeo.util/ext/test/org/argeo/util/CsvParserWithQuotedSeparatorTest.java [new file with mode: 0644]
org.argeo.util/ext/test/org/argeo/util/CsvWriterTestCase.java [new file with mode: 0644]
org.argeo.util/ext/test/org/argeo/util/ReferenceFile.csv [new file with mode: 0644]
org.argeo.util/ext/test/org/argeo/util/TestParse-ISO.csv [new file with mode: 0644]
org.argeo.util/ext/test/org/argeo/util/TestParse-UTF-8.csv [new file with mode: 0644]
org.argeo.util/ext/test/org/argeo/util/ThroughputTest.java [new file with mode: 0644]
org.argeo.util/pom.xml [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/CsvParser.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/CsvParserWithLinesAsMap.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/CsvWriter.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/DictionaryKeys.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/DigestUtils.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/DirH.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/LangUtils.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/PasswordEncryption.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/StreamUtils.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/Throughput.java [new file with mode: 0644]
org.argeo.util/src/org/argeo/util/UtilsException.java [new file with mode: 0644]
pom.xml [new file with mode: 0644]
rcp/.gitignore [new file with mode: 0644]
rcp/demo/.gitignore [new file with mode: 0644]
rcp/demo/argeo-companion.properties [new file with mode: 0644]
rcp/demo/log4j.properties [new file with mode: 0644]
rcp/dep/.gitignore [new file with mode: 0644]
rcp/dep/org.argeo.dep.cms.e4.rcp/.gitignore [new file with mode: 0644]
rcp/dep/org.argeo.dep.cms.e4.rcp/META-INF/.gitignore [new file with mode: 0644]
rcp/dep/org.argeo.dep.cms.e4.rcp/bnd.bnd [new file with mode: 0644]
rcp/dep/org.argeo.dep.cms.e4.rcp/p2.inf [new file with mode: 0644]
rcp/dep/org.argeo.dep.cms.e4.rcp/pom.xml [new file with mode: 0644]
rcp/dep/org.argeo.dep.cms.e4.rcp/src/assembly/linux.x86.xml [new file with mode: 0644]
rcp/dep/org.argeo.dep.cms.e4.rcp/src/assembly/linux.x86_64.xml [new file with mode: 0644]
rcp/dep/org.argeo.dep.cms.e4.rcp/src/assembly/win32.x86.xml [new file with mode: 0644]
rcp/dep/pom.xml [new file with mode: 0644]
rcp/dist/argeo-companion/rpm/etc/argeo-companion/argeo-companion.ini [new file with mode: 0644]
rcp/dist/argeo-companion/rpm/etc/argeo-companion/log4j.properties [new file with mode: 0644]
rcp/dist/argeo-companion/rpm/usr/bin/argeo-companion [new file with mode: 0755]
rcp/org.argeo.cms.e4.rcp/.classpath [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/.gitignore [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/.project [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/.settings/org.eclipse.pde.core.prefs [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/META-INF/.gitignore [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/argeo-companion.e4xmi [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/bnd.bnd [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/build.properties [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/plugin.xml [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/pom.xml [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/src/org/argeo/cms/e4/rcp/CmsE4Application.java [new file with mode: 0644]
rcp/org.argeo.cms.e4.rcp/src/org/argeo/cms/e4/rcp/CmsRcpLifeCycle.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/.classpath [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/.gitignore [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/.project [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/META-INF/.gitignore [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/bnd.bnd [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/build.properties [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/pom.xml [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpClient.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpResourceManager.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/DefaultNLS.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/EclipseUiSpecificUtils.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/OpenFile.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/SingleSourcingException.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/UiContext.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileDetails.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadEvent.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadHandler.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadListener.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadReceiver.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/RWT.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/SingletonUtil.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/AbstractEntryPoint.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/Application.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/ApplicationConfiguration.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/EntryPoint.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/EntryPointFactory.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/ExceptionHandler.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/Client.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/WebClient.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/BrowserNavigation.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/BrowserNavigationEvent.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/BrowserNavigationListener.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/ClientService.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/JavaScriptExecutor.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/UrlLauncher.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/service/ResourceLoader.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/service/ResourceManager.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/service/ServerPushSession.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/widgets/DropDown.java [new file with mode: 0644]
rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/widgets/FileUpload.java [new file with mode: 0644]
rcp/pom.xml [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..b83d222
--- /dev/null
@@ -0,0 +1 @@
+/target/
diff --git a/.project b/.project
new file mode 100644 (file)
index 0000000..2bb4833
--- /dev/null
+++ b/.project
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>argeo-commons</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+       </buildSpec>
+       <natures>
+       </natures>
+</projectDescription>
diff --git a/demo/.gitignore b/demo/.gitignore
new file mode 100644 (file)
index 0000000..0980598
--- /dev/null
@@ -0,0 +1,2 @@
+/exec/
+/target/
diff --git a/demo/all.policy b/demo/all.policy
new file mode 100644 (file)
index 0000000..facb613
--- /dev/null
@@ -0,0 +1,3 @@
+grant {
+  permission java.security.AllPermission;
+};
\ No newline at end of file
diff --git a/demo/argeo_node.js b/demo/argeo_node.js
new file mode 100755 (executable)
index 0000000..ac8671f
--- /dev/null
@@ -0,0 +1,15 @@
+#!/home/mbaudier/dev/git/apache2/argeo-commons/dist/osgi-boot/src/main/rpm/usr/bin/a2jjs
+// demo specific
+var app = "node";
+var demoHome = $ENV.HOME + "/dev/git/apache2/argeo-commons/demo";
+var appHome = demoHome + "/exec/argeo_node.js";
+var appConf = demoHome;
+var policyFile = "all.policy";
+
+load("../dist/argeo-node/rpm/usr/share/node/jjs/cms.js");
+osgi.baseUrl = "http://forge.argeo.org/data/java/argeo-2.1/";
+osgi.install("org.argeo.commons:org.argeo.dep.cms.platform:2.1.71-SNAPSHOT");
+osgi.httpPort = 0;
+//osgi.clean = true;
+osgi.launch();
+openWorkbench();
diff --git a/demo/argeo_node_cli.properties b/demo/argeo_node_cli.properties
new file mode 100644 (file)
index 0000000..1c33311
--- /dev/null
@@ -0,0 +1,8 @@
+argeo.osgi.start.1.node=\
+org.springframework.osgi.extender,\
+
+argeo.osgi.start.3.node=\
+org.argeo.node.repo.jackrabbit,\
+org.argeo.security.dao.cli,\
+
+log4j.configuration=file:../../log4j.properties
diff --git a/demo/argeo_node_cluster_0.properties b/demo/argeo_node_cluster_0.properties
new file mode 100644 (file)
index 0000000..2b25750
--- /dev/null
@@ -0,0 +1,39 @@
+argeo.osgi.start.2.node=\
+org.eclipse.equinox.http.servlet,\
+org.eclipse.equinox.http.jetty,\
+org.eclipse.equinox.metatype,\
+org.eclipse.equinox.cm,\
+org.eclipse.rap.rwt.osgi
+
+argeo.osgi.start.3.node=\
+org.argeo.cms
+
+argeo.osgi.start.4.apps=\
+org.eclipse.gemini.blueprint.extender
+
+argeo.osgi.start.4.workbench=\
+org.eclipse.equinox.http.registry,\
+
+java.security.manager=
+java.security.policy=file:../../all.policy
+
+argeo.node.repo.type=postgresql_cluster_ds
+argeo.node.repo.clusterId=03233754-16c3-49a1-8a00-58bf89a65182
+argeo.node.repo.dburl=jdbc:postgresql://localhost/argeo_node_cluster
+argeo.node.repo.dbuser=argeo
+argeo.node.repo.dbpassword=argeo
+
+# HTTP
+org.osgi.service.http.port=7070
+
+# i18n
+argeo.i18n.locales=en,fr,ru
+eclipse.registry.MultiLanguage=true
+#argeo.i18n.defaultLocale=en
+
+# Logging
+log4j.configuration=file:../../log4j.properties
+
+# DON'T CHANGE BELOW
+org.eclipse.rap.workbenchAutostart=false
+org.eclipse.equinox.http.jetty.autostart=false
\ No newline at end of file
diff --git a/demo/argeo_node_cluster_1.properties b/demo/argeo_node_cluster_1.properties
new file mode 100644 (file)
index 0000000..2d35be9
--- /dev/null
@@ -0,0 +1,39 @@
+argeo.osgi.start.2.node=\
+org.eclipse.equinox.http.servlet,\
+org.eclipse.equinox.http.jetty,\
+org.eclipse.equinox.metatype,\
+org.eclipse.equinox.cm,\
+org.eclipse.rap.rwt.osgi
+
+argeo.osgi.start.3.node=\
+org.argeo.cms
+
+argeo.osgi.start.4.apps=\
+org.eclipse.gemini.blueprint.extender
+
+argeo.osgi.start.4.workbench=\
+org.eclipse.equinox.http.registry,\
+
+java.security.manager=
+java.security.policy=file:../../all.policy
+
+argeo.node.repo.type=postgresql_cluster_ds
+argeo.node.repo.clusterId=52463fa3-2917-4814-9ff7-685c41cbc7c7
+argeo.node.repo.dburl=jdbc:postgresql://localhost/argeo_node_cluster
+argeo.node.repo.dbuser=argeo
+argeo.node.repo.dbpassword=argeo
+
+# HTTP
+org.osgi.service.http.port=7071
+
+# i18n
+argeo.i18n.locales=en,fr,ru
+eclipse.registry.MultiLanguage=true
+#argeo.i18n.defaultLocale=en
+
+# Logging
+log4j.configuration=file:../../log4j.properties
+
+# DON'T CHANGE BELOW
+org.eclipse.rap.workbenchAutostart=false
+org.eclipse.equinox.http.jetty.autostart=false
\ No newline at end of file
diff --git a/demo/argeo_node_cms.properties b/demo/argeo_node_cms.properties
new file mode 100644 (file)
index 0000000..a11d723
--- /dev/null
@@ -0,0 +1,40 @@
+argeo.osgi.start.2.http=\
+org.eclipse.equinox.http.servlet,\
+org.eclipse.equinox.http.jetty,\
+org.eclipse.equinox.metatype,\
+org.eclipse.rap.rwt.osgi,\
+org.eclipse.equinox.cm
+
+argeo.osgi.start.3.node=\
+org.argeo.cms,\
+
+argeo.osgi.start.4.node=\
+org.eclipse.gemini.blueprint.extender
+
+#argeo.osgi.start.4.cms=\
+#org.argeo.cms.core
+
+#argeo.osgi.start.4.workbench=\
+#org.eclipse.equinox.http.registry,\
+
+argeo.osgi.start.5.cms=\
+org.argeo.cms.demo
+
+argeo.node.repo.type=localfs
+
+org.osgi.service.http.port=7070
+#org.osgi.service.http.port.secure=7073
+org.eclipse.equinox.http.jetty.log.stderr.threshold=info
+
+java.security.manager=
+java.security.policy=file:../../all.policy
+
+# i18n
+argeo.i18n.locales=en,fr
+eclipse.registry.MultiLanguage=true
+
+log4j.configuration=file:../../log4j.properties
+
+# DON'T CHANGE BELOW
+org.eclipse.rap.workbenchAutostart=false
+org.eclipse.equinox.http.jetty.autostart=false
diff --git a/demo/argeo_node_local.properties b/demo/argeo_node_local.properties
new file mode 100644 (file)
index 0000000..8273754
--- /dev/null
@@ -0,0 +1,38 @@
+argeo.osgi.start.2.node=\
+org.eclipse.equinox.http.servlet,\
+org.eclipse.equinox.http.jetty,\
+org.eclipse.equinox.metatype,\
+org.eclipse.equinox.cm,\
+org.eclipse.rap.rwt.osgi
+
+argeo.osgi.start.3.node=\
+org.argeo.cms
+
+argeo.osgi.start.4.apps=\
+org.eclipse.gemini.blueprint.extender
+
+argeo.osgi.start.4.workbench=\
+org.eclipse.equinox.http.registry,\
+
+java.security.manager=
+java.security.policy=file:../../all.policy
+
+argeo.node.repo.type=h2
+
+argeo.node.useradmin.uris=os:///
+
+# HTTP
+org.osgi.service.http.port=7070
+
+# Logging
+log4j.configuration=file:../../log4j.properties
+
+# DON'T CHANGE BELOW
+org.eclipse.rap.workbenchAutostart=false
+org.eclipse.equinox.http.jetty.autostart=false
+org.osgi.framework.bootdelegation=com.sun.jndi.ldap,\
+com.sun.jndi.ldap.sasl,\
+com.sun.security.jgss,\
+com.sun.jndi.dns,\
+com.sun.nio.file,\
+com.sun.nio.sctp
diff --git a/demo/argeo_node_osgiboot.properties b/demo/argeo_node_osgiboot.properties
new file mode 100644 (file)
index 0000000..64ff5d5
--- /dev/null
@@ -0,0 +1,21 @@
+argeo.osgi.baseUrl=http://forge.argeo.org/data/java/argeo-2.1/
+argeo.osgi.distributionUrl=org/argeo/commons/org.argeo.dep.cms.sdk/2.1.65/org.argeo.dep.cms.sdk-2.1.65.jar
+#argeo.osgi.distributionUrl=org/argeo/commons/org.argeo.dep.cms.sdk/2.1.67/org.argeo.dep.cms.sdk-2.1.67.jar
+#argeo.osgi.distributionUrl=org/argeo/commons/org.argeo.dep.cms.sdk/2.1.68-SNAPSHOT/org.argeo.dep.cms.sdk-2.1.68-SNAPSHOT.jar
+
+argeo.osgi.boot.debug=true
+
+argeo.osgi.start.1.osgiboot=org.argeo.osgi.boot
+argeo.osgi.start.2.node=org.eclipse.equinox.http.servlet,org.eclipse.equinox.http.jetty,org.eclipse.equinox.cm,org.eclipse.rap.rwt.osgi
+argeo.osgi.start.3.node=org.argeo.cms,org.eclipse.gemini.blueprint.extender,org.eclipse.equinox.http.registry
+
+java.security.manager=
+java.security.policy=file:../../all.policy
+
+argeo.node.repo.type=localfs
+org.osgi.service.http.port=7070
+log4j.configuration=file:../../log4j.properties
+
+# DON'T CHANGE BELOW
+org.eclipse.rap.workbenchAutostart=false
+org.eclipse.equinox.http.jetty.autostart=false
\ No newline at end of file
diff --git a/demo/argeo_node_rap.properties b/demo/argeo_node_rap.properties
new file mode 100644 (file)
index 0000000..b8169e3
--- /dev/null
@@ -0,0 +1,72 @@
+argeo.osgi.start.2.node=\
+org.eclipse.equinox.http.servlet,\
+org.eclipse.equinox.http.jetty,\
+org.eclipse.equinox.metatype,\
+org.eclipse.equinox.cm,\
+org.eclipse.rap.rwt.osgi
+
+argeo.osgi.start.3.node=\
+org.argeo.cms
+
+argeo.osgi.start.4.eclipse3=\
+org.eclipse.equinox.http.registry,\
+org.eclipse.gemini.blueprint.extender
+
+#argeo.osgi.start.5.web=\
+#org.argeo.cms.demo,\
+
+java.security.manager=
+java.security.policy=file:../../all.policy
+
+argeo.node.repo.type=h2
+#argeo.node.transaction.manager=bitronix
+
+#argeo.node.useradmin.uris=ldap://cn=Directory%20Manager:argeoargeo@localhost:10389/dc=example,dc=com
+
+#argeo.node.useradmin.uris="\
+#ldap://uid=admin,ou=system:secret\
+#@localhost:10389\
+#/dc=example,dc=com\
+#?readOnly=false\
+#&userObjectClass=inetOrgPerson \
+#dc=example,dc=org.ldif"
+
+# HTTP
+org.osgi.service.http.port=7070
+#org.eclipse.equinox.http.jetty.log.stderr.threshold=info
+
+# HTTPS
+#org.osgi.service.http.port.secure=7073
+
+# In order to configure demo certificates, run:
+# cd ssl; sh ./ssl.sh;
+
+# i18n
+argeo.i18n.locales=en,fr,ru
+eclipse.registry.MultiLanguage=true
+#argeo.i18n.defaultLocale=en
+
+# Logging
+log4j.configuration=file:../../log4j.properties
+
+# Tuning
+# Number of DB connections
+#argeo.node.repo.maxPoolSize=10
+# Max amount of memory available to Jackrabbit caches
+#argeo.node.repo.maxCacheMB=16
+# Persistence level cache
+#argeo.node.repo.bundleCacheMB=8
+# Search, see http://wiki.apache.org/jackrabbit/Search
+#argeo.node.repo.extractorPoolSize=0
+#argeo.node.repo.searchCacheSize=1000
+#argeo.node.repo.maxVolatileIndexSize=1048576
+
+# DON'T CHANGE BELOW
+org.eclipse.rap.workbenchAutostart=false
+org.eclipse.equinox.http.jetty.autostart=false
+org.osgi.framework.bootdelegation=com.sun.jndi.ldap,\
+com.sun.jndi.ldap.sasl,\
+com.sun.security.jgss,\
+com.sun.jndi.dns,\
+com.sun.nio.file,\
+com.sun.nio.sctp
diff --git a/demo/cms-e4-rap.properties b/demo/cms-e4-rap.properties
new file mode 100644 (file)
index 0000000..f9158f0
--- /dev/null
@@ -0,0 +1,31 @@
+argeo.osgi.start.2.node=\
+org.eclipse.equinox.http.servlet,\
+org.eclipse.equinox.http.jetty,\
+org.eclipse.equinox.metatype,\
+org.eclipse.equinox.cm,\
+org.eclipse.equinox.ds,\
+org.eclipse.rap.rwt.osgi
+
+argeo.osgi.start.3.node=\
+org.argeo.cms
+
+argeo.osgi.start.5.node=\
+org.argeo.cms.e4.rap
+
+# Local
+argeo.node.repo.type=h2
+org.osgi.service.http.port=7070
+#org.osgi.service.http.port.secure=7073
+
+# Logging
+log4j.configuration=file:../../log4j.properties
+#log4j.configuration=file:log4j.properties
+
+# DON'T CHANGE BELOW
+org.eclipse.equinox.http.jetty.autostart=false
+org.osgi.framework.bootdelegation=com.sun.jndi.ldap,\
+com.sun.jndi.ldap.sasl,\
+com.sun.security.jgss,\
+com.sun.jndi.dns,\
+com.sun.nio.file,\
+com.sun.nio.sctp
diff --git a/demo/init/node/.gitignore b/demo/init/node/.gitignore
new file mode 100644 (file)
index 0000000..f619744
--- /dev/null
@@ -0,0 +1,4 @@
+/krb5.keytab
+/krb5.keytab.old
+/*.p12
+/*.jks
\ No newline at end of file
diff --git a/demo/log4j.properties b/demo/log4j.properties
new file mode 100644 (file)
index 0000000..05932ba
--- /dev/null
@@ -0,0 +1,14 @@
+log4j.rootLogger=WARN, development
+
+log4j.logger.org.argeo=DEBUG
+log4j.logger.org.argeo.cms.internal=TRACE
+log4j.logger.org.argeo.cms.viewers=TRACE
+
+## Appenders
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern= %-5p %d{ISO8601} %m - %c - [%t]%n
+
+log4j.appender.development=org.apache.log4j.ConsoleAppender
+log4j.appender.development.layout=org.apache.log4j.PatternLayout
+log4j.appender.development.layout.ConversionPattern=%d{ABSOLUTE} %m (%F:%L) [%t] %p %n
diff --git a/demo/pom.xml b/demo/pom.xml
new file mode 100644 (file)
index 0000000..ebfa8f2
--- /dev/null
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>demo</artifactId>
+       <name>Commons Demo</name>
+       <packaging>pom</packaging>
+       <build>
+       </build>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.osgi</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.osgi.boot</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+       </dependencies>
+       <profiles>
+               <profile>
+                       <id>argeo_node_rap</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <groupId>org.argeo.maven.plugins</groupId>
+                                               <artifactId>maven-argeo-osgi-plugin</artifactId>
+                                               <configuration>
+                                                       <systemPropertiesFile>argeo_node_rap.properties</systemPropertiesFile>
+                                                       <execDir>exec/argeo_node_rap</execDir>
+                                               </configuration>
+                                       </plugin>
+                               </plugins>
+                       </build>
+                       <dependencies>
+                               <dependency>
+                                       <groupId>org.argeo.commons</groupId>
+                                       <artifactId>org.argeo.dep.cms.sdk</artifactId>
+                                       <version>2.1.76-SNAPSHOT</version>
+                               </dependency>
+                       </dependencies>
+               </profile>
+               <profile>
+                       <id>cms-e4-rap</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <groupId>org.argeo.maven.plugins</groupId>
+                                               <artifactId>maven-argeo-osgi-plugin</artifactId>
+                                               <configuration>
+                                                       <systemPropertiesFile>cms-e4-rap.properties</systemPropertiesFile>
+                                                       <execDir>exec/cms-e4-rap</execDir>
+                                               </configuration>
+                                       </plugin>
+                               </plugins>
+                       </build>
+                       <dependencies>
+                               <dependency>
+                                       <groupId>org.argeo.commons</groupId>
+                                       <artifactId>org.argeo.dep.cms.e4.rap</artifactId>
+                                       <version>2.1.76-SNAPSHOT</version>
+                               </dependency>
+                       </dependencies>
+               </profile>
+       </profiles>
+</project>
\ No newline at end of file
diff --git a/demo/ssl/.gitignore b/demo/ssl/.gitignore
new file mode 100644 (file)
index 0000000..bc77402
--- /dev/null
@@ -0,0 +1,7 @@
+/CA/
+/*.p12
+/*.jks
+/nssdb/
+/*.pem
+/old/
+/rootCA/
diff --git a/demo/ssl/openssl.cnf b/demo/ssl/openssl.cnf
new file mode 100644 (file)
index 0000000..05bb6f7
--- /dev/null
@@ -0,0 +1,120 @@
+dir            = ./CA          # Where everything is kept
+
+[ ca ]
+default_ca     = CA_default            # The default ca section
+
+[ CA_default ]
+certs          = $dir/certs            # Where the issued certs are kept
+crl_dir                = $dir/crl              # Where the issued crl are kept
+database       = $dir/index.txt        # database index file.
+new_certs_dir  = $dir/newcerts         # default place for new certs.
+certificate    = $dir/cacert.pem       # The CA certificate
+serial         = $dir/serial           # The current serial number
+crlnumber      = $dir/crlnumber        # the current crl number
+crl            = $dir/crl.pem          # The current CRL
+private_key    = $dir/private/cakey.pem # The private key
+x509_extensions        = usr_cert              # The extentions to add to the cert
+name_opt       = ca_default            # Subject Name options
+cert_opt       = ca_default            # Certificate field options
+crl_extensions = crl_ext
+default_days   = 365                   # how long to certify for
+default_crl_days= 30                   # how long before next CRL
+default_md     = default               # use public key default MD
+preserve       = no                    # keep passed DN ordering
+policy         = policy_match
+
+[ policy_match ]
+countryName            = optional
+stateOrProvinceName    = optional
+organizationName       = optional
+organizationalUnitName = optional
+commonName             = optional
+emailAddress           = optional
+
+[ policy_anything ]
+countryName            = optional
+stateOrProvinceName    = optional
+localityName           = optional
+organizationName       = optional
+organizationalUnitName = optional
+commonName             = optional
+emailAddress           = optional
+
+[ req ]
+default_bits           = 4096
+default_md             = sha1
+default_keyfile        = privkey.pem
+distinguished_name     = req_distinguished_name
+attributes             = req_attributes
+x509_extensions        = v3_ca # The extensions to add to the self signed cert
+
+# Passwords for private keys if not present they will be prompted for
+input_password = demo
+output_password = demo
+
+string_mask = utf8only
+req_extensions = v3_req # The extensions to add to a certificate request
+
+[ req_distinguished_name ]
+countryName                    = Country Name (2 letter code)
+countryName_min                        = 2
+countryName_max                        = 2
+#stateOrProvinceName           = State or Province Name (full name)
+#localityName                  = Locality Name (eg, city)
+0.organizationName             = Organization Name (eg, company)
+organizationalUnitName         = Organizational Unit Name (eg, section)
+commonName                     = Common Name (eg, your name or your server\'s hostname)
+commonName_max                 = 64
+emailAddress                   = Email Address
+emailAddress_max               = 64
+# SET-ex3                      = SET extension number 3
+
+##
+## DEFAULT VALUES
+##
+countryName_default            = DE
+#stateOrProvinceName_default   = Berlin
+#localityName_default  = Berlin
+0.organizationName_default     = Example
+organizationalUnitName_default = Certificate Authorities
+commonName_default     = Intermediate CA
+
+[ req_attributes ]
+#challengePassword             = A challenge password
+#challengePassword_min         = 4
+#challengePassword_max         = 20
+#unstructuredName              = An optional company name
+
+[ usr_cert ]
+basicConstraints=CA:FALSE
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid,issuer
+subjectAltName=email:move
+issuerAltName=issuer:copy
+
+[ v3_req ]
+basicConstraints = CA:FALSE
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+
+[ v3_ca ]
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid:always,issuer
+basicConstraints = critical, CA:true
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+[ v3_intermediate_ca ]
+# Extensions for a typical intermediate CA (`man x509v3_config`).
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical, CA:true, pathlen:0
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+[ crl_ext ]
+issuerAltName=issuer:copy
+authorityKeyIdentifier=keyid:always
+
+[ server_ext ]
+extendedKeyUsage=serverAuth
+
+[ user_ext ]
+extendedKeyUsage=clientAuth,emailProtection
diff --git a/demo/ssl/openssl_root.cnf b/demo/ssl/openssl_root.cnf
new file mode 100644 (file)
index 0000000..c689459
--- /dev/null
@@ -0,0 +1,120 @@
+dir            = ./rootCA              # Where everything is kept
+
+[ ca ]
+default_ca     = CA_default            # The default ca section
+
+[ CA_default ]
+certs          = $dir/certs            # Where the issued certs are kept
+crl_dir                = $dir/crl              # Where the issued crl are kept
+database       = $dir/index.txt        # database index file.
+new_certs_dir  = $dir/newcerts         # default place for new certs.
+certificate    = $dir/cacert.pem       # The CA certificate
+serial         = $dir/serial           # The current serial number
+crlnumber      = $dir/crlnumber        # the current crl number
+crl            = $dir/crl.pem          # The current CRL
+private_key    = $dir/private/cakey.pem # The private key
+x509_extensions        = usr_cert              # The extentions to add to the cert
+name_opt       = ca_default            # Subject Name options
+cert_opt       = ca_default            # Certificate field options
+crl_extensions = crl_ext
+default_days   = 3650          # how long to certify for
+default_crl_days= 30                   # how long before next CRL
+default_md     = default               # use public key default MD
+preserve       = no                    # keep passed DN ordering
+policy         = policy_match
+
+[ policy_match ]
+countryName            = optional
+stateOrProvinceName    = optional
+organizationName       = optional
+organizationalUnitName = optional
+commonName             = optional
+emailAddress           = optional
+
+[ policy_anything ]
+countryName            = optional
+stateOrProvinceName    = optional
+localityName           = optional
+organizationName       = optional
+organizationalUnitName = optional
+commonName             = optional
+emailAddress           = optional
+
+[ req ]
+default_bits           = 4096
+default_md             = sha1
+default_keyfile        = privkey.pem
+distinguished_name     = req_distinguished_name
+attributes             = req_attributes
+x509_extensions        = v3_ca # The extensions to add to the self signed cert
+
+# Passwords for private keys if not present they will be prompted for
+input_password = demo
+output_password = demo
+
+string_mask = utf8only
+req_extensions = v3_req # The extensions to add to a certificate request
+
+[ req_distinguished_name ]
+countryName                    = Country Name (2 letter code)
+countryName_min                        = 2
+countryName_max                        = 2
+#stateOrProvinceName           = State or Province Name (full name)
+#localityName                  = Locality Name (eg, city)
+0.organizationName             = Organization Name (eg, company)
+organizationalUnitName         = Organizational Unit Name (eg, section)
+commonName                     = Common Name (eg, your name or your server\'s hostname)
+commonName_max                 = 64
+emailAddress                   = Email Address
+emailAddress_max               = 64
+# SET-ex3                      = SET extension number 3
+
+##
+## DEFAULT VALUES
+##
+countryName_default            = DE
+#stateOrProvinceName_default   = Berlin
+#localityName_default  = Berlin
+0.organizationName_default     = Example
+organizationalUnitName_default = Certificate Authorities
+commonName_default     = Root CA
+
+[ req_attributes ]
+#challengePassword             = A challenge password
+#challengePassword_min         = 4
+#challengePassword_max         = 20
+#unstructuredName              = An optional company name
+
+[ usr_cert ]
+basicConstraints=CA:FALSE
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid,issuer
+subjectAltName=email:move
+issuerAltName=issuer:copy
+
+[ v3_req ]
+basicConstraints = CA:FALSE
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+
+[ v3_ca ]
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid:always,issuer
+basicConstraints = critical, CA:true
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+[ v3_intermediate_ca ]
+# Extensions for a typical intermediate CA (`man x509v3_config`).
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical, CA:true, pathlen:0
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+[ crl_ext ]
+issuerAltName=issuer:copy
+authorityKeyIdentifier=keyid:always
+
+[ server_ext ]
+extendedKeyUsage=serverAuth
+
+[ user_ext ]
+extendedKeyUsage=clientAuth,emailProtection
diff --git a/demo/ssl/ssl.sh b/demo/ssl/ssl.sh
new file mode 100644 (file)
index 0000000..46b72d8
--- /dev/null
@@ -0,0 +1,82 @@
+#!/bin/sh
+
+# COMPLETELY UNSAFE - FOR DEVELOPMENT ONLY
+# Run this script from its directory
+# all *.p12 passwords are 'demo'
+# all *.jks passwords are 'changeit'
+
+INTERMEDIATE_CA_DN="/C=DE/O=Example/OU=Certificate Authorities/CN=Intermediate CA/"
+SERVER_DN=/C=DE/O=Example/OU=Systems/CN=$HOSTNAME/
+USERS_BASE_DN=/DC=com/DC=example/OU=People
+
+echo ## Init directory structure
+# Root
+export OPENSSL_CONF=./openssl_root.cnf
+export CATOP=./rootCA
+/etc/pki/tls/misc/CA -newca
+# Intermediate
+mkdir -p ./CA/{certs,crl,csr,newcerts,private}
+
+echo ## Create intermediate certificate
+openssl req -new -newkey rsa:4096 -extensions v3_intermediate_ca \
+ -subj "$INTERMEDIATE_CA_DN" \
+ -keyout ./CA/private/cakey.pem -passout pass:demo -out ica_csr.pem
+openssl ca -batch -passin pass:demo -in ica_csr.pem -out ./CA/cacert.pem
+
+# create index and serial
+touch ./CA/index.txt
+# (below is from openssl CA script)
+openssl x509 -in ./CA/cacert.pem -noout -next_serial -out ./CA/serial
+
+# Switch to intermediate CA                  
+export OPENSSL_CONF=./openssl.cnf
+export CATOP=./CA
+
+echo ## Create server key and certificate
+openssl req -new -newkey rsa:4096 -extensions server_ext \
+ -subj $SERVER_DN \
+ -keyout node_key.pem -passout pass:demo -out node_csr.pem
+openssl ca -batch -passin pass:demo -in node_csr.pem -out node_crt.pem
+cat node_crt.pem ./CA/cacert.pem ./rootCA/cacert.pem > chain.pem
+openssl pkcs12 -export -passin pass:demo -passout pass:changeit \
+ -name "$HOSTNAME" -inkey node_key.pem -in chain.pem \
+ -out node.p12
+
+echo ## Import Certificate Authority into keystore
+keytool -importcert -noprompt -keystore node.p12 -storepass changeit \
+ -alias "rootCA" -file ./rootCA/cacert.pem
+keytool -importcert -noprompt -keystore node.p12 -storepass changeit \
+ -alias "CA" -file ./CA/cacert.pem
+cp node.p12 ../init/node/
+
+echo ## Create 'root' user client certificate
+openssl req -new -newkey rsa:4096 -extensions user_ext \
+ -subj $USERS_BASE_DN/UID=root/ \
+ -keyout newkey.pem -passout pass:demo -out newcsr.pem
+openssl ca -preserveDN -batch -passin pass:demo -in newcsr.pem -out newcrt.pem
+cat newcrt.pem ./CA/cacert.pem ./rootCA/cacert.pem > newchain.pem
+openssl pkcs12 -export -passin pass:demo -passout pass:demo \
+ -name "root" -inkey newkey.pem -in newchain.pem \
+ -out root.p12
+
+# demo user
+#openssl req -new -newkey rsa:4096 -extensions user_ext -days 365 \
+# -subj $USERS_BASE_DN/UID=demo/ \
+# -keyout newkey.pem -passout pass:demo -out newcsr.pem
+#openssl ca -preserveDN -batch -passin pass:demo -in newcsr.pem -out newcrt.pem
+#openssl pkcs12 -export -passin pass:demo -passout pass:demo \
+# -name "demo" -inkey newkey.pem -in newcrt.pem \
+# -out demo.p12
+
+# Self-signed
+#openssl req -x509 -new -newkey rsa:4096 -extensions server_ext -days 365 \
+# -subj $SERVER_DN \
+# -keyout newkey.pem -passout pass:demo -out newcrt.pem
+# Self-signed server certificate
+#openssl pkcs12 -export -passin pass:demo -passout pass:changeit \
+# -name "jetty" -inkey newkey.pem -in newcrt.pem \
+# -certfile ./CA/cacert.pem \
+# -out server.p12
+
+echo ## Clean up
+rm -vf *.pem
diff --git a/dep/.gitignore b/dep/.gitignore
new file mode 100644 (file)
index 0000000..b83d222
--- /dev/null
@@ -0,0 +1 @@
+/target/
diff --git a/dep/org.argeo.dep.cms.client/.gitignore b/dep/org.argeo.dep.cms.client/.gitignore
new file mode 100644 (file)
index 0000000..e26e09f
--- /dev/null
@@ -0,0 +1,4 @@
+/target/
+/feature.xml
+/modularDistribution.csv
+/*-maven.target
diff --git a/dep/org.argeo.dep.cms.client/META-INF/.gitignore b/dep/org.argeo.dep.cms.client/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/dep/org.argeo.dep.cms.client/bnd.bnd b/dep/org.argeo.dep.cms.client/bnd.bnd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/dep/org.argeo.dep.cms.client/build.properties b/dep/org.argeo.dep.cms.client/build.properties
new file mode 100644 (file)
index 0000000..edef3d9
--- /dev/null
@@ -0,0 +1,2 @@
+bin.includes = feature.xml,\
+               modularDistribution.csv
diff --git a/dep/org.argeo.dep.cms.client/p2.inf b/dep/org.argeo.dep.cms.client/p2.inf
new file mode 100644 (file)
index 0000000..0423aa5
--- /dev/null
@@ -0,0 +1,2 @@
+properties.1.name=org.eclipse.equinox.p2.type.category
+properties.1.value=true
\ No newline at end of file
diff --git a/dep/org.argeo.dep.cms.client/pom.xml b/dep/org.argeo.dep.cms.client/pom.xml
new file mode 100644 (file)
index 0000000..51eed51
--- /dev/null
@@ -0,0 +1,413 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>dep</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.dep.cms.client</artifactId>
+       <name>CMS Client</name>
+       <dependencies>
+
+               <!-- Argeo Commons -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.util</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.jcr</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- Third Parties -->
+               <dependency>
+                       <groupId>org.argeo.tp.javax</groupId>
+                       <artifactId>javax.jcr</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.javax</groupId>
+                       <artifactId>javax.transaction</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.log4j</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>com.jcraft.jsch</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>org.slf4j.log4j12</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>org.slf4j.api</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>org.slf4j.commons.logging</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.bouncycastle</groupId>
+                       <artifactId>bcprov</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.bouncycastle</groupId>
+                       <artifactId>bcpkix</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.httpcomponents.httpcore</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.httpcomponents.httpclient</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.io</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.codec</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.exec</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.httpclient</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.vfs</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.net</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.collections</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.dbcp</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.pool</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.compress</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.javax</groupId>
+                       <artifactId>javax.servlet</artifactId>
+               </dependency>
+
+               <!-- Equinox -->
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.osgi.util</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.util</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.cm</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.osgi.services</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.registry</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.preferences</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.common</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.event</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.app</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.ds</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.metatype</artifactId>
+               </dependency>
+
+               <!-- SSH -->
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.mina.core</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.sshd.core</artifactId>
+               </dependency>
+
+               <!-- Console -->
+               <dependency>
+                       <groupId>org.argeo.tp.apache.felix</groupId>
+                       <artifactId>org.apache.felix.scr</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.felix</groupId>
+                       <artifactId>org.apache.felix.gogo.runtime</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.felix</groupId>
+                       <artifactId>org.apache.felix.gogo.shell</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.console</artifactId>
+               </dependency>
+
+               <!-- Jackrabbit client -->
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.api</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.jcr.commons</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.spi</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.spi.commons</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.webdav</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.spi2dav</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.jcr2dav</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.jcr2spi</artifactId>
+               </dependency>
+
+               <!-- Required by Jackrabbit 2.12 -->
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>com.google.guava</artifactId>
+               </dependency>
+
+               <!-- Test only -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.osgi.boot</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <scope>test</scope>
+               </dependency>
+       </dependencies>
+
+       <profiles>
+               <profile>
+                       <id>rpmbuild</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                                       <!-- <plugin> -->
+                                       <!-- <groupId>org.apache.maven.plugins</groupId> -->
+                                       <!-- <artifactId>maven-dependency-plugin</artifactId> -->
+                                       <!-- <executions> -->
+                                       <!-- <execution> -->
+                                       <!-- <id>copy-argeo</id> -->
+                                       <!-- <phase>package</phase> -->
+                                       <!-- <goals> -->
+                                       <!-- <goal>copy-dependencies</goal> -->
+                                       <!-- </goals> -->
+                                       <!-- <configuration> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <outputDirectory>${project.build.directory}/lib-argeo</outputDirectory> -->
+                                       <!-- <includeGroupIds>org.argeo.commons</includeGroupIds> -->
+                                       <!-- <excludeTransitive>true</excludeTransitive> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <includeScope>runtime</includeScope> -->
+                                       <!-- <useRepositoryLayout>true</useRepositoryLayout> -->
+                                       <!-- </configuration> -->
+                                       <!-- </execution> -->
+                                       <!-- </executions> -->
+                                       <!-- </plugin> -->
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-argeo</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-client</name>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <!-- <mapping> -->
+                                                                               <!-- <directory>/usr/share/osgi/org/argeo/commons/${project.artifactId}/${project.version}</directory> -->
+                                                                               <!-- <username>root</username> -->
+                                                                               <!-- <groupname>root</groupname> -->
+                                                                               <!-- <directoryIncluded>false</directoryIncluded> -->
+                                                                               <!-- <artifact /> -->
+                                                                               <!-- </mapping> -->
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>argeo-cms-client-tp</require>
+                                                                               <require>osgi-boot</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+               <profile>
+                       <id>rpmbuild-tp</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source-tp</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                                       <!-- <plugin> -->
+                                       <!-- <groupId>org.apache.maven.plugins</groupId> -->
+                                       <!-- <artifactId>maven-dependency-plugin</artifactId> -->
+                                       <!-- <executions> -->
+                                       <!-- <execution> -->
+                                       <!-- <id>copy-tp</id> -->
+                                       <!-- <phase>package</phase> -->
+                                       <!-- <goals> -->
+                                       <!-- <goal>copy-dependencies</goal> -->
+                                       <!-- </goals> -->
+                                       <!-- <configuration> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <outputDirectory>${project.build.directory}/lib-tp</outputDirectory> -->
+                                       <!-- <excludeGroupIds>org.argeo.commons</excludeGroupIds> -->
+                                       <!-- <excludeTransitive>true</excludeTransitive> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <includeScope>runtime</includeScope> -->
+                                       <!-- <useRepositoryLayout>true</useRepositoryLayout> -->
+                                       <!-- </configuration> -->
+                                       <!-- </execution> -->
+                                       <!-- </executions> -->
+                                       <!-- </plugin> -->
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-client-tp</name>
+                                                                       <projversion>${version.argeo-tp}</projversion>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source-tp</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                       </mappings>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
+</project>
\ No newline at end of file
diff --git a/dep/org.argeo.dep.cms.e4.rap/.gitignore b/dep/org.argeo.dep.cms.e4.rap/.gitignore
new file mode 100644 (file)
index 0000000..7bfff59
--- /dev/null
@@ -0,0 +1,4 @@
+/target/
+/feature.xml
+/modularDistribution.csv
+/*.target
diff --git a/dep/org.argeo.dep.cms.e4.rap/META-INF/.gitignore b/dep/org.argeo.dep.cms.e4.rap/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/dep/org.argeo.dep.cms.e4.rap/bnd.bnd b/dep/org.argeo.dep.cms.e4.rap/bnd.bnd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/dep/org.argeo.dep.cms.e4.rap/build.properties b/dep/org.argeo.dep.cms.e4.rap/build.properties
new file mode 100644 (file)
index 0000000..edef3d9
--- /dev/null
@@ -0,0 +1,2 @@
+bin.includes = feature.xml,\
+               modularDistribution.csv
diff --git a/dep/org.argeo.dep.cms.e4.rap/p2.inf b/dep/org.argeo.dep.cms.e4.rap/p2.inf
new file mode 100644 (file)
index 0000000..0423aa5
--- /dev/null
@@ -0,0 +1,2 @@
+properties.1.name=org.eclipse.equinox.p2.type.category
+properties.1.value=true
\ No newline at end of file
diff --git a/dep/org.argeo.dep.cms.e4.rap/pom.xml b/dep/org.argeo.dep.cms.e4.rap/pom.xml
new file mode 100644 (file)
index 0000000..79f8513
--- /dev/null
@@ -0,0 +1,511 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>dep</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.dep.cms.e4.rap</artifactId>
+       <name>CMS Platform Eclipse 4 RAP</name>
+       <dependencies>
+
+               <!-- Argeo Commons -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.dep.cms.node</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <type>pom</type>
+               </dependency>
+
+               <!-- RWT -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.rwt</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.commands</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.jface</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.filedialog</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.fileupload</artifactId>
+               </dependency>
+
+               <!-- Argeo Commons UI -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui.theme</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- E4 Specific -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.e4</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.e4.rap</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- Misc Third Parties -->
+               <dependency>
+                       <groupId>org.argeo.tp.bouncycastle</groupId>
+                       <artifactId>bcmail</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.bouncycastle</groupId>
+                       <artifactId>bcpg</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.ant</groupId>
+                       <artifactId>org.apache.ant</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.ant</groupId>
+                       <artifactId>org.apache.ant.launch</artifactId>
+               </dependency>
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.misc</groupId> -->
+               <!-- <artifactId>org.quartz-scheduler.quartz</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.misc</groupId> -->
+               <!-- <artifactId>org.quartz-scheduler.quartz.jobs</artifactId> -->
+               <!-- </dependency> -->
+               <dependency>
+                       <groupId>org.argeo.tp.javax</groupId>
+                       <artifactId>javax.mail</artifactId>
+               </dependency>
+
+               <!-- Nebula -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.nebula.widgets.richtext</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.nebula.widgets.grid</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.nebula.jface.gridviewer</artifactId>
+               </dependency>
+
+
+               <!-- Spring -->
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.aspects</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.context.support</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.jdbc</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.tx</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.web</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.web.servlet</artifactId>
+               </dependency>
+
+               <!-- Eclipse Core -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.databinding</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.databinding.beans</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.runtime</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.databinding.property</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>com.ibm.icu</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.contenttype</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.rwt.osgi</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.jface.databinding</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.jobs</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.expressions</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.databinding.observable</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.help</artifactId>
+               </dependency>
+
+               <!-- RAP Workbench -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.rap.platform</groupId> -->
+               <!-- <artifactId>org.eclipse.rap.ui</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.rap.platform</groupId> -->
+               <!-- <artifactId>org.eclipse.rap.ui.forms</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.rap.platform</groupId> -->
+               <!-- <artifactId>org.eclipse.rap.ui.views</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.rap.platform</groupId> -->
+               <!-- <artifactId>org.eclipse.rap.ui.workbench</artifactId> -->
+               <!-- </dependency> -->
+
+
+               <!-- Dependencies required / provided by Eclipse 4 -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.apache.commons.jxpath</artifactId>
+               </dependency>
+
+               <!-- Eclipse 4 -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.e4</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.emf.common</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.emf.ecore</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.emf.ecore.change</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.emf.ecore.xmi</artifactId>
+               </dependency>
+
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.workbench.renderers.swt</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.di</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.workbench.addons.swt</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.commands</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.bindings</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.workbench.swt</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di.extensions.supplier</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.model.workbench</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.emf.xpath</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.contexts</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.services</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di.annotations</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.services</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.fileupload</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di.extensions</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.workbench</artifactId>
+               </dependency>
+
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp</groupId> -->
+               <!-- <artifactId>argeo-tp-rap-e4</artifactId> -->
+               <!-- <version>${version.argeo-tp}</version> -->
+               <!-- <scope>provided</scope> -->
+               <!-- </dependency> -->
+
+               <!-- SDK -->
+               <dependency>
+                       <groupId>org.argeo.tp.sdk</groupId>
+                       <artifactId>org.junit</artifactId>
+                       <scope>test</scope>
+               </dependency>
+
+       </dependencies>
+       <dependencyManagement>
+       </dependencyManagement>
+       <profiles>
+               <profile>
+                       <id>rpmbuild</id>
+                       <build>
+                               <plugins>
+                                       <!-- <plugin> -->
+                                       <!-- <groupId>org.apache.maven.plugins</groupId> -->
+                                       <!-- <artifactId>maven-dependency-plugin</artifactId> -->
+                                       <!-- <executions> -->
+                                       <!-- <execution> -->
+                                       <!-- <id>copy-argeo</id> -->
+                                       <!-- <phase>package</phase> -->
+                                       <!-- <goals> -->
+                                       <!-- <goal>copy-dependencies</goal> -->
+                                       <!-- </goals> -->
+                                       <!-- <configuration> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <outputDirectory>${project.build.directory}/lib-argeo</outputDirectory> -->
+                                       <!-- <includeGroupIds>org.argeo.commons</includeGroupIds> -->
+                                       <!-- <excludeTransitive>true</excludeTransitive> -->
+                                       <!-- <excludeArtifactIds>org.argeo.dep.cms.node</excludeArtifactIds> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <includeScope>runtime</includeScope> -->
+                                       <!-- <useRepositoryLayout>true</useRepositoryLayout> -->
+                                       <!-- </configuration> -->
+                                       <!-- </execution> -->
+                                       <!-- </executions> -->
+                                       <!-- </plugin> -->
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-argeo</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-e4-rap</name>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <!-- <mapping> -->
+                                                                               <!-- <directory>/usr/share/osgi/org/argeo/commons/${project.artifactId}/${project.version}</directory> -->
+                                                                               <!-- <username>root</username> -->
+                                                                               <!-- <groupname>root</groupname> -->
+                                                                               <!-- <directoryIncluded>false</directoryIncluded> -->
+                                                                               <!-- <artifact /> -->
+                                                                               <!-- </mapping> -->
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>argeo-cms-node</require>
+                                                                               <require>argeo-cms-e4-rap-tp</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+               <profile>
+                       <id>rpmbuild-tp</id>
+                       <build>
+                               <plugins>
+                                       <!-- <plugin> -->
+                                       <!-- <groupId>org.apache.maven.plugins</groupId> -->
+                                       <!-- <artifactId>maven-dependency-plugin</artifactId> -->
+                                       <!-- <executions> -->
+                                       <!-- <execution> -->
+                                       <!-- <id>copy-tp</id> -->
+                                       <!-- <phase>package</phase> -->
+                                       <!-- <goals> -->
+                                       <!-- <goal>copy-dependencies</goal> -->
+                                       <!-- </goals> -->
+                                       <!-- <configuration> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <outputDirectory>${project.build.directory}/lib-tp</outputDirectory> -->
+                                       <!-- <excludeGroupIds>org.argeo.commons</excludeGroupIds> -->
+                                       <!-- <excludeTransitive>true</excludeTransitive> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <includeScope>runtime</includeScope> -->
+                                       <!-- <useRepositoryLayout>true</useRepositoryLayout> -->
+                                       <!-- </configuration> -->
+                                       <!-- </execution> -->
+                                       <!-- </executions> -->
+                                       <!-- </plugin> -->
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source-tp</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-e4-rap-tp</name>
+                                                                       <projversion>${version.argeo-tp}</projversion>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source-tp</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>argeo-cms-node-tp</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
+
+</project>
\ No newline at end of file
diff --git a/dep/org.argeo.dep.cms.node/.gitignore b/dep/org.argeo.dep.cms.node/.gitignore
new file mode 100644 (file)
index 0000000..e26e09f
--- /dev/null
@@ -0,0 +1,4 @@
+/target/
+/feature.xml
+/modularDistribution.csv
+/*-maven.target
diff --git a/dep/org.argeo.dep.cms.node/META-INF/.gitignore b/dep/org.argeo.dep.cms.node/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/dep/org.argeo.dep.cms.node/bnd.bnd b/dep/org.argeo.dep.cms.node/bnd.bnd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/dep/org.argeo.dep.cms.node/build.properties b/dep/org.argeo.dep.cms.node/build.properties
new file mode 100644 (file)
index 0000000..edef3d9
--- /dev/null
@@ -0,0 +1,2 @@
+bin.includes = feature.xml,\
+               modularDistribution.csv
diff --git a/dep/org.argeo.dep.cms.node/p2.inf b/dep/org.argeo.dep.cms.node/p2.inf
new file mode 100644 (file)
index 0000000..0423aa5
--- /dev/null
@@ -0,0 +1,2 @@
+properties.1.name=org.eclipse.equinox.p2.type.category
+properties.1.value=true
\ No newline at end of file
diff --git a/dep/org.argeo.dep.cms.node/pom.xml b/dep/org.argeo.dep.cms.node/pom.xml
new file mode 100644 (file)
index 0000000..199e168
--- /dev/null
@@ -0,0 +1,423 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>dep</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.dep.cms.node</artifactId>
+       <name>CMS Node</name>
+       <dependencies>
+
+               <!-- Parent dependencies -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.dep.cms.client</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <type>pom</type>
+               </dependency>
+
+               <!-- Argeo Commons -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.node.api</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.enterprise</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.ext.jackrabbit</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- CMS Dependencies -->
+               <!-- TODO: not bitronix dependent -->
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>bitronix.tm</artifactId>
+               </dependency>
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.misc</groupId> -->
+               <!-- <artifactId>org.joda.time</artifactId> -->
+               <!-- </dependency> -->
+
+               <!-- Javax -->
+               <dependency>
+                       <groupId>org.argeo.tp.javax</groupId>
+                       <artifactId>javax.annotation</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.javax</groupId>
+                       <artifactId>javax.inject</artifactId>
+               </dependency>
+
+               <!-- Database drivers -->
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>org.h2</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>org.postgresql.jdbc42</artifactId>
+               </dependency>
+               <!-- Third Parties -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.misc</groupId> -->
+               <!-- <artifactId>org.w3c.css.sac</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.misc</groupId> -->
+               <!-- <artifactId>com.steadystate.css</artifactId> -->
+               <!-- </dependency> -->
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>com.google.gson</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.xmlgraphics.commons</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>org.w3c.dom.svg</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.batik.i18n</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.batik.util</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.batik.css</artifactId>
+               </dependency>
+
+               <!-- Jackrabbit -->
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.data</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.core</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.jackrabbit</groupId>
+                       <artifactId>org.apache.jackrabbit.server</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>EDU.oswego.cs.dl.util.concurrent</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.commons</groupId>
+                       <artifactId>org.apache.commons.fileupload</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.tika.core</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.tika.parsers</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache</groupId>
+                       <artifactId>org.apache.lucene</artifactId>
+               </dependency>
+
+               <!-- TODO: remove Spring dependency -->
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.beans</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.core</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.javax</groupId>
+                       <artifactId>javax.el</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.gemini</groupId>
+                       <artifactId>org.eclipse.gemini.blueprint.core</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.gemini</groupId>
+                       <artifactId>org.eclipse.gemini.blueprint.extender</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.gemini</groupId>
+                       <artifactId>org.eclipse.gemini.blueprint.io</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>org.aspectj.weaver</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>org.aopalliance</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.aop</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.context</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.expression</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.instrument</artifactId>
+               </dependency>
+
+               <!-- HTTP Server -->
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.http.servlet</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.http.jetty</artifactId>
+               </dependency>
+
+               <!-- Jetty -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.jetty</groupId> -->
+               <!-- <artifactId>org.eclipse.jetty.client</artifactId> -->
+               <!-- </dependency> -->
+               <dependency>
+                       <groupId>org.argeo.tp.jetty</groupId>
+                       <artifactId>org.eclipse.jetty.continuation</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.jetty</groupId>
+                       <artifactId>org.eclipse.jetty.http</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.jetty</groupId>
+                       <artifactId>org.eclipse.jetty.io</artifactId>
+               </dependency>
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.jetty</groupId> -->
+               <!-- <artifactId>org.eclipse.jetty.jmx</artifactId> -->
+               <!-- </dependency> -->
+               <dependency>
+                       <groupId>org.argeo.tp.jetty</groupId>
+                       <artifactId>org.eclipse.jetty.security</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.jetty</groupId>
+                       <artifactId>org.eclipse.jetty.server</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.jetty</groupId>
+                       <artifactId>org.eclipse.jetty.servlet</artifactId>
+               </dependency>
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.jetty</groupId> -->
+               <!-- <artifactId>org.eclipse.jetty.servlets</artifactId> -->
+               <!-- </dependency> -->
+               <dependency>
+                       <groupId>org.argeo.tp.jetty</groupId>
+                       <artifactId>org.eclipse.jetty.util</artifactId>
+               </dependency>
+
+       </dependencies>
+
+       <profiles>
+               <profile>
+                       <id>rpmbuild</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+
+                                       <!-- <plugin> -->
+                                       <!-- <groupId>org.apache.maven.plugins</groupId> -->
+                                       <!-- <artifactId>maven-dependency-plugin</artifactId> -->
+                                       <!-- <executions> -->
+                                       <!-- <execution> -->
+                                       <!-- <id>copy-argeo</id> -->
+                                       <!-- <phase>package</phase> -->
+                                       <!-- <goals> -->
+                                       <!-- <goal>copy-dependencies</goal> -->
+                                       <!-- </goals> -->
+                                       <!-- <configuration> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <outputDirectory>${project.build.directory}/lib-argeo</outputDirectory> -->
+                                       <!-- <includeGroupIds>org.argeo.commons</includeGroupIds> -->
+                                       <!-- <excludeTransitive>true</excludeTransitive> -->
+                                       <!-- <excludeArtifactIds>org.argeo.dep.cms.client</excludeArtifactIds> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <includeScope>runtime</includeScope> -->
+                                       <!-- <useRepositoryLayout>true</useRepositoryLayout> -->
+                                       <!-- </configuration> -->
+                                       <!-- </execution> -->
+                                       <!-- </executions> -->
+                                       <!-- </plugin> -->
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-argeo</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-node</name>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <!-- <mapping> -->
+                                                                               <!-- <directory>/usr/share/osgi/org/argeo/commons/${project.artifactId}/${project.version}</directory> -->
+                                                                               <!-- <username>root</username> -->
+                                                                               <!-- <groupname>root</groupname> -->
+                                                                               <!-- <directoryIncluded>false</directoryIncluded> -->
+                                                                               <!-- <artifact /> -->
+                                                                               <!-- </mapping> -->
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>argeo-cms-client</require>
+                                                                               <require>argeo-cms-node-tp</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+               <profile>
+                       <id>rpmbuild-tp</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source-tp</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                                       <!-- <plugin> -->
+                                       <!-- <groupId>org.apache.maven.plugins</groupId> -->
+                                       <!-- <artifactId>maven-dependency-plugin</artifactId> -->
+                                       <!-- <executions> -->
+                                       <!-- <execution> -->
+                                       <!-- <id>copy-tp</id> -->
+                                       <!-- <phase>package</phase> -->
+                                       <!-- <goals> -->
+                                       <!-- <goal>copy-dependencies</goal> -->
+                                       <!-- </goals> -->
+                                       <!-- <configuration> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <outputDirectory>${project.build.directory}/lib-tp</outputDirectory> -->
+                                       <!-- <excludeGroupIds>org.argeo.commons</excludeGroupIds> -->
+                                       <!-- <excludeTransitive>true</excludeTransitive> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <includeScope>runtime</includeScope> -->
+                                       <!-- <useRepositoryLayout>true</useRepositoryLayout> -->
+                                       <!-- </configuration> -->
+                                       <!-- </execution> -->
+                                       <!-- </executions> -->
+                                       <!-- </plugin> -->
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-node-tp</name>
+                                                                       <projversion>${version.argeo-tp}</projversion>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source-tp</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>argeo-cms-client-tp</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
+</project>
\ No newline at end of file
diff --git a/dep/org.argeo.dep.cms.platform/.gitignore b/dep/org.argeo.dep.cms.platform/.gitignore
new file mode 100644 (file)
index 0000000..e26e09f
--- /dev/null
@@ -0,0 +1,4 @@
+/target/
+/feature.xml
+/modularDistribution.csv
+/*-maven.target
diff --git a/dep/org.argeo.dep.cms.platform/META-INF/.gitignore b/dep/org.argeo.dep.cms.platform/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/dep/org.argeo.dep.cms.platform/bnd.bnd b/dep/org.argeo.dep.cms.platform/bnd.bnd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/dep/org.argeo.dep.cms.platform/build.properties b/dep/org.argeo.dep.cms.platform/build.properties
new file mode 100644 (file)
index 0000000..edef3d9
--- /dev/null
@@ -0,0 +1,2 @@
+bin.includes = feature.xml,\
+               modularDistribution.csv
diff --git a/dep/org.argeo.dep.cms.platform/p2.inf b/dep/org.argeo.dep.cms.platform/p2.inf
new file mode 100644 (file)
index 0000000..0423aa5
--- /dev/null
@@ -0,0 +1,2 @@
+properties.1.name=org.eclipse.equinox.p2.type.category
+properties.1.value=true
\ No newline at end of file
diff --git a/dep/org.argeo.dep.cms.platform/pom.xml b/dep/org.argeo.dep.cms.platform/pom.xml
new file mode 100644 (file)
index 0000000..79e6da8
--- /dev/null
@@ -0,0 +1,406 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>dep</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.dep.cms.platform</artifactId>
+       <name>CMS Platform</name>
+       <dependencies>
+
+               <!-- Parent dependencies -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.dep.cms.node</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <type>pom</type>
+               </dependency>
+
+               <!-- RWT -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.rwt</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.commands</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.jface</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.filedialog</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.fileupload</artifactId>
+               </dependency>
+
+               <!-- Argeo Commons UI -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui.theme</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- Eclipse 3 specific -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui.workbench</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui.workbench.rap</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.ext.rap.ui.workbench</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- Misc Third Parties -->
+<!--           <dependency> -->
+<!--                   <groupId>org.argeo.tp.apache</groupId> -->
+<!--                   <artifactId>org.apache.tika.parser</artifactId> -->
+<!--           </dependency> -->
+               <dependency>
+                       <groupId>org.argeo.tp.bouncycastle</groupId>
+                       <artifactId>bcmail</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.bouncycastle</groupId>
+                       <artifactId>bcpg</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.ant</groupId>
+                       <artifactId>org.apache.ant</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.ant</groupId>
+                       <artifactId>org.apache.ant.launch</artifactId>
+               </dependency>
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.misc</groupId> -->
+               <!-- <artifactId>org.quartz-scheduler.quartz</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.misc</groupId> -->
+               <!-- <artifactId>org.quartz-scheduler.quartz.jobs</artifactId> -->
+               <!-- </dependency> -->
+               <dependency>
+                       <groupId>org.argeo.tp.javax</groupId>
+                       <artifactId>javax.mail</artifactId>
+               </dependency>
+
+               <!-- Spring -->
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.aspects</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.context.support</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.jdbc</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.tx</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.web</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.spring</groupId>
+                       <artifactId>org.springframework.web.servlet</artifactId>
+               </dependency>
+
+               <!-- Equinox HTTP registry is required by Eclipse 3 but causes problem 
+                       with Eclipse 4 -->
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.equinox.http.registry</artifactId>
+               </dependency>
+
+               <!-- Eclipse Core -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.core.databinding</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.core.databinding.beans</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.core.runtime</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.core.databinding.property</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>com.ibm.icu</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.core.contenttype</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.rap.rwt.osgi</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.rap.jface.databinding</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.core.jobs</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.core.expressions</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.core.databinding.observable</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.help</artifactId>
+               </dependency>
+
+               <!-- RAP Workbench -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.rap.ui</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.rap.ui.forms</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.rap.ui.views</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.platform</groupId>
+                       <artifactId>org.eclipse.rap.ui.workbench</artifactId>
+               </dependency>
+       </dependencies>
+       <dependencyManagement>
+               <dependencies>
+                       <dependency>
+                               <groupId>org.argeo.tp</groupId>
+                               <artifactId>argeo-tp-rap-e3</artifactId>
+                               <version>${version.argeo-tp}</version>
+                               <type>pom</type>
+                               <scope>import</scope>
+                       </dependency>
+               </dependencies>
+       </dependencyManagement>
+
+       <profiles>
+               <profile>
+                       <id>rpmbuild</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                                       <!-- <plugin> -->
+                                       <!-- <groupId>org.apache.maven.plugins</groupId> -->
+                                       <!-- <artifactId>maven-dependency-plugin</artifactId> -->
+                                       <!-- <executions> -->
+                                       <!-- <execution> -->
+                                       <!-- <id>copy-argeo</id> -->
+                                       <!-- <phase>package</phase> -->
+                                       <!-- <goals> -->
+                                       <!-- <goal>copy-dependencies</goal> -->
+                                       <!-- </goals> -->
+                                       <!-- <configuration> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <outputDirectory>${project.build.directory}/lib-argeo</outputDirectory> -->
+                                       <!-- <includeGroupIds>org.argeo.commons</includeGroupIds> -->
+                                       <!-- <excludeTransitive>true</excludeTransitive> -->
+                                       <!-- <excludeArtifactIds>org.argeo.dep.cms.node</excludeArtifactIds> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <includeScope>runtime</includeScope> -->
+                                       <!-- <useRepositoryLayout>true</useRepositoryLayout> -->
+                                       <!-- </configuration> -->
+                                       <!-- </execution> -->
+                                       <!-- </executions> -->
+                                       <!-- </plugin> -->
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-argeo</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-platform</name>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <!-- <mapping> -->
+                                                                               <!-- <directory>/usr/share/osgi/org/argeo/commons/${project.artifactId}/${project.version}</directory> -->
+                                                                               <!-- <username>root</username> -->
+                                                                               <!-- <groupname>root</groupname> -->
+                                                                               <!-- <directoryIncluded>false</directoryIncluded> -->
+                                                                               <!-- <artifact /> -->
+                                                                               <!-- </mapping> -->
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>argeo-cms-node</require>
+                                                                               <require>argeo-cms-platform-tp</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+               <profile>
+                       <id>rpmbuild-tp</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source-tp</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                                       <!-- <plugin> -->
+                                       <!-- <groupId>org.apache.maven.plugins</groupId> -->
+                                       <!-- <artifactId>maven-dependency-plugin</artifactId> -->
+                                       <!-- <executions> -->
+                                       <!-- <execution> -->
+                                       <!-- <id>copy-tp</id> -->
+                                       <!-- <phase>package</phase> -->
+                                       <!-- <goals> -->
+                                       <!-- <goal>copy-dependencies</goal> -->
+                                       <!-- </goals> -->
+                                       <!-- <configuration> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <outputDirectory>${project.build.directory}/lib-tp</outputDirectory> -->
+                                       <!-- <excludeGroupIds>org.argeo.commons</excludeGroupIds> -->
+                                       <!-- <excludeTransitive>true</excludeTransitive> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <includeScope>runtime</includeScope> -->
+                                       <!-- <useRepositoryLayout>true</useRepositoryLayout> -->
+                                       <!-- </configuration> -->
+                                       <!-- </execution> -->
+                                       <!-- </executions> -->
+                                       <!-- </plugin> -->
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-platform-tp</name>
+                                                                       <projversion>${version.argeo-tp}</projversion>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source-tp</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>argeo-cms-node-tp</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
+</project>
\ No newline at end of file
diff --git a/dep/org.argeo.dep.cms.sdk/.gitignore b/dep/org.argeo.dep.cms.sdk/.gitignore
new file mode 100644 (file)
index 0000000..e26e09f
--- /dev/null
@@ -0,0 +1,4 @@
+/target/
+/feature.xml
+/modularDistribution.csv
+/*-maven.target
diff --git a/dep/org.argeo.dep.cms.sdk/META-INF/.gitignore b/dep/org.argeo.dep.cms.sdk/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/dep/org.argeo.dep.cms.sdk/bnd.bnd b/dep/org.argeo.dep.cms.sdk/bnd.bnd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/dep/org.argeo.dep.cms.sdk/build.properties b/dep/org.argeo.dep.cms.sdk/build.properties
new file mode 100644 (file)
index 0000000..edef3d9
--- /dev/null
@@ -0,0 +1,2 @@
+bin.includes = feature.xml,\
+               modularDistribution.csv
diff --git a/dep/org.argeo.dep.cms.sdk/p2.inf b/dep/org.argeo.dep.cms.sdk/p2.inf
new file mode 100644 (file)
index 0000000..0423aa5
--- /dev/null
@@ -0,0 +1,2 @@
+properties.1.name=org.eclipse.equinox.p2.type.category
+properties.1.value=true
\ No newline at end of file
diff --git a/dep/org.argeo.dep.cms.sdk/pom.xml b/dep/org.argeo.dep.cms.sdk/pom.xml
new file mode 100644 (file)
index 0000000..7561e45
--- /dev/null
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>dep</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.dep.cms.sdk</artifactId>
+       <name>CMS SDK</name>
+       <dependencies>
+               <!-- Parent dependencies -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.dep.cms.platform</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <type>pom</type>
+               </dependency>
+
+               <!-- ALM Third Parties -->
+               <dependency>
+                       <groupId>org.argeo.tp.apache.ant</groupId>
+                       <artifactId>org.apache.ant</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.apache.ant</groupId>
+                       <artifactId>org.apache.ant.launch</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.sdk</groupId>
+                       <artifactId>biz.aQute.bnd</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.sdk</groupId>
+                       <artifactId>org.junit</artifactId>
+               </dependency>
+
+               <!-- SLC -->
+               <dependency>
+                       <groupId>org.argeo.tp.sdk</groupId>
+                       <artifactId>org.dbunit</artifactId>
+               </dependency>
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.aether</groupId> -->
+               <!-- <artifactId>org.eclipse.aether.api</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.aether</groupId> -->
+               <!-- <artifactId>org.eclipse.aether.spi</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.aether</groupId> -->
+               <!-- <artifactId>org.eclipse.aether.util</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.aether</groupId> -->
+               <!-- <artifactId>org.eclipse.aether.impl</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.aether</groupId> -->
+               <!-- <artifactId>org.eclipse.aether.connector.basic</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.aether</groupId> -->
+               <!-- <artifactId>org.eclipse.aether.transport.classpath</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.aether</groupId> -->
+               <!-- <artifactId>org.eclipse.aether.transport.file</artifactId> -->
+               <!-- </dependency> -->
+               <dependency>
+                       <groupId>org.argeo.tp.sdk</groupId>
+                       <artifactId>org.redline-rpm</artifactId>
+               </dependency>
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.sdk</groupId> -->
+               <!-- <artifactId>org.tmatesoft.svnkit</artifactId> -->
+               <!-- </dependency> -->
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>com.googlecode.javaewah.JavaEWAH</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.misc</groupId>
+                       <artifactId>org.eclipse.jgit</artifactId>
+               </dependency>
+
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.misc</groupId> -->
+               <!-- <artifactId>org.postgresql.postgresql</artifactId> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.misc</groupId> -->
+               <!-- <artifactId>org.h2</artifactId> -->
+               <!-- </dependency> -->
+       </dependencies>
+
+       <profiles>
+               <profile>
+                       <id>rpmbuild-tp</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source-tp</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                                       <!-- <plugin> -->
+                                       <!-- <groupId>org.apache.maven.plugins</groupId> -->
+                                       <!-- <artifactId>maven-dependency-plugin</artifactId> -->
+                                       <!-- <executions> -->
+                                       <!-- <execution> -->
+                                       <!-- <id>copy-tp</id> -->
+                                       <!-- <phase>package</phase> -->
+                                       <!-- <goals> -->
+                                       <!-- <goal>copy-dependencies</goal> -->
+                                       <!-- </goals> -->
+                                       <!-- <configuration> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <outputDirectory>${project.build.directory}/lib-tp</outputDirectory> -->
+                                       <!-- <excludeGroupIds>org.argeo.commons</excludeGroupIds> -->
+                                       <!-- <excludeTransitive>true</excludeTransitive> -->
+                                       <!-- <excludeArtifactIds>org.argeo.dep.cms.platform</excludeArtifactIds> -->
+                                       <!-- <includeTypes>jar</includeTypes> -->
+                                       <!-- <includeScope>runtime</includeScope> -->
+                                       <!-- <useRepositoryLayout>true</useRepositoryLayout> -->
+                                       <!-- </configuration> -->
+                                       <!-- </execution> -->
+                                       <!-- </executions> -->
+                                       <!-- </plugin> -->
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-sdk-tp</name>
+                                                                       <projversion>${version.argeo-tp}</projversion>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source-tp</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>argeo-cms-platform-tp</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
+</project>
\ No newline at end of file
diff --git a/dep/pom.xml b/dep/pom.xml
new file mode 100644 (file)
index 0000000..392b6c2
--- /dev/null
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>dep</artifactId>
+       <name>Commons Modular Distributions</name>
+       <packaging>pom</packaging>
+       <modules>
+               <module>org.argeo.dep.cms.client</module>
+               <module>org.argeo.dep.cms.node</module>
+               <module>org.argeo.dep.cms.platform</module>
+               <module>org.argeo.dep.cms.e4.rap</module>
+               <module>org.argeo.dep.cms.sdk</module>
+       </modules>
+       <build>
+               <plugins>
+                       <plugin>
+                               <groupId>org.apache.felix</groupId>
+                               <artifactId>maven-bundle-plugin</artifactId>
+                               <configuration>
+                                       <instructions>
+                                               <SLC-ModularDistribution>default</SLC-ModularDistribution>
+                                       </instructions>
+                               </configuration>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.argeo.maven.plugins</groupId>
+                               <artifactId>maven-argeo-osgi-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               <id>generate-descriptors</id>
+                                               <goals>
+                                                       <goal>descriptors</goal>
+                                               </goals>
+                                               <phase>generate-resources</phase>
+                                       </execution>
+                               </executions>
+                       </plugin>
+                       <plugin>
+                               <artifactId>maven-assembly-plugin</artifactId>
+                               <dependencies>
+                                       <dependency>
+                                               <groupId>org.argeo.commons</groupId>
+                                               <artifactId>assembly-descriptors</artifactId>
+                                               <version>2.1.76-SNAPSHOT</version>
+                                       </dependency>
+                               </dependencies>
+                               <configuration>
+                                       <attach>false</attach>
+                               </configuration>
+                       </plugin>
+               </plugins>
+       </build>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.osgi</artifactId>
+                       <scope>test</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.sdk</groupId>
+                       <artifactId>org.junit</artifactId>
+                       <scope>test</scope>
+               </dependency>
+       </dependencies>
+       <profiles>
+               <profile>
+                       <id>check-osgi</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <groupId>org.argeo.maven.plugins</groupId>
+                                               <artifactId>maven-argeo-osgi-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>check-osgi</id>
+                                                               <phase>test</phase>
+                                                               <goals>
+                                                                       <goal>equinox</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <onlyCheck>true</onlyCheck>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+                       <dependencies>
+                               <dependency>
+                                       <groupId>org.argeo.commons</groupId>
+                                       <artifactId>org.argeo.osgi.boot</artifactId>
+                                       <version>2.1.76-SNAPSHOT</version>
+                                       <scope>test</scope>
+                               </dependency>
+                       </dependencies>
+               </profile>
+       </profiles>
+</project>
\ No newline at end of file
diff --git a/dist/.gitignore b/dist/.gitignore
new file mode 100644 (file)
index 0000000..66cc710
--- /dev/null
@@ -0,0 +1,2 @@
+/target/
+
diff --git a/dist/argeo-node/.gitignore b/dist/argeo-node/.gitignore
new file mode 100644 (file)
index 0000000..b83d222
--- /dev/null
@@ -0,0 +1 @@
+/target/
diff --git a/dist/argeo-node/assembly/cms-e4-rap.xml b/dist/argeo-node/assembly/cms-e4-rap.xml
new file mode 100644 (file)
index 0000000..c474f3c
--- /dev/null
@@ -0,0 +1,38 @@
+<assembly
+       xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+       <id>dist</id>
+       <baseDirectory></baseDirectory>
+       <formats>
+               <format>zip</format>
+       </formats>
+       <fileSets>
+               <fileSet>
+                       <directory>base</directory>
+                       <outputDirectory></outputDirectory>
+                       <fileMode>0644</fileMode>
+                       <includes>
+                               <include>**</include>
+                       </includes>
+                       <excludes>
+                               <exclude>offline.bat</exclude>
+                       </excludes>
+               </fileSet>
+       </fileSets>
+<!--   <dependencySets> -->
+<!--           <dependencySet> -->
+<!--                   <unpack>false</unpack> -->
+<!--                   <outputFileNameMapping>${artifact.groupId}/${artifact.artifactId}-${artifact.version}.${artifact.extension}</outputFileNameMapping> -->
+<!--                   <outputDirectory>share/osgi</outputDirectory> -->
+<!--           </dependencySet> -->
+<!--           <dependencySet> -->
+<!--                   <useStrictFiltering>true</useStrictFiltering> -->
+<!--                   <unpack>true</unpack> -->
+<!--                   <outputDirectory></outputDirectory> -->
+<!--                   <includes> -->
+<!--                           <include>org.argeo.commons:osgi-boot:zip:*:*</include> -->
+<!--                   </includes> -->
+<!--           </dependencySet> -->
+<!--   </dependencySets> -->
+</assembly>
\ No newline at end of file
diff --git a/dist/argeo-node/base/bin/argeo-cms b/dist/argeo-node/base/bin/argeo-cms
new file mode 100755 (executable)
index 0000000..a1701a3
--- /dev/null
@@ -0,0 +1,133 @@
+#!/bin/sh
+APP=argeo
+
+JVM=java
+
+BIN_DIR=`dirname "$0"`
+BASE_DIR=$BIN_DIR/..
+
+# Directories and files
+CONF_DIR=$BASE_DIR/etc/$APP
+CONF_DIRS=$CONF_DIR/conf.d
+#BASE_POLICY_ALL=/usr/share/$APP/all.policy
+BASE_CONFIG_INI=$BASE_DIR/share/$APP/config.ini
+
+#EXEC_DIR=$BASE_DIR/var/lib/$APP
+EXEC_DIR=.
+DATA_DIR=$EXEC_DIR/data
+CONF_RW=$EXEC_DIR/state
+CONFIG_INI=$CONF_RW/config.ini
+
+OSGI_INSTALL_AREA=$BASE_DIR/share/osgi/boot
+OSGI_FRAMEWORK=$OSGI_INSTALL_AREA/org.eclipse.osgi.jar
+
+# Overwrite variables
+if [ -f $CONF_DIR/settings.sh ];then
+       . $CONF_DIR/settings.sh
+fi
+
+RETVAL=0
+
+start() {
+       mkdir -p $CONF_RW
+       mkdir -p $DATA_DIR
+
+    # Merge config files
+    printf "## Equinox configuration - Generated by /usr/sbin/nodectl ##\n\n" > $CONFIG_INI
+    cat $BASE_CONFIG_INI >> $CONFIG_INI
+    printf "\n##\n## $CONF_DIR/$APP.ini\n##\n\n" >> $CONFIG_INI
+    cat $CONF_DIR/$APP.ini >> $CONFIG_INI
+    for file in `ls -v $CONF_DIRS/*.ini`; do
+            printf "\n##\n## $file\n##\n\n" >> $CONFIG_INI
+            cat $file >> $CONFIG_INI
+    done;
+
+       cd $EXEC_DIR
+       $JVM \
+               -Dlog4j.configuration="file:$CONF_DIR/log4j.properties" \
+               $JAVA_OPTS -jar $OSGI_FRAMEWORK \
+               -configuration "$CONF_RW" \
+               -data "$DATA_DIR"
+}
+
+reload() {
+       echo Not yet implemented
+}
+
+stop() {
+       if [ -f $PID_FILE ];then
+               PID=`cat $PID_FILE`
+               kill -0 $PID &> /dev/null
+               PID_EXISTS=$?
+               if [ $PID_EXISTS -ne 0 ]; then
+                       echo Dead $APP process with pid $PID, removing $PID_FILE
+                       rm -f $PID_FILE
+                       RETVAL=1
+                       return $RETVAL
+               fi
+       else
+               echo $APP is not running
+               RETVAL=1
+               return $RETVAL
+       fi
+       
+       # notifies application by removing the shutdown file
+#      rm -f $SHUTDOWN_FILE
+       kill $PID
+       
+       # wait 10 min for application to shutdown, then kill it
+       TIMEOUT=$((10*60))
+       BEGIN=$(date +%s)
+       while kill -0 $PID &> /dev/null
+       do
+               sleep 1
+               NOW=$(date +%s)
+               DURATION=$(($NOW-$BEGIN))
+               if [ $DURATION -gt $TIMEOUT ]; then
+                       kill -9 $PID
+                       echo Forcibly killed $APP with pid $PID
+                       RETVAL=1
+               fi
+       done
+       
+       # remove pid file
+       rm -f $PID_FILE
+       return $RETVAL
+}
+
+status() {
+       if [ -f $PID_FILE ];then
+               PID=`cat $PID_FILE`
+       else
+               echo $APP is not running
+               return $RETVAL
+       fi
+       kill -0 $PID &> /dev/null
+       PID_EXISTS=$?
+       if [ $PID_EXISTS -eq 0 ]; then
+               echo $APP is running with pid $PID ...
+       else
+               echo No $APP process with pid $PID, removing $PID_FILE
+               rm -f $PID_FILE
+       fi
+       return $RETVAL
+}
+
+# main
+case "$1" in
+  start)
+        start
+        ;;
+  reload)
+        reload
+        ;;
+  stop)
+        stop
+        ;;
+  status)
+       status
+        ;;
+  *)
+        start
+        ;;
+esac
\ No newline at end of file
diff --git a/dist/argeo-node/base/bin/argeo-cms.js b/dist/argeo-node/base/bin/argeo-cms.js
new file mode 100644 (file)
index 0000000..01a12d7
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/a2jjs\r
+load("share/argeo/cms.js");\r
+osgi.httpPort = 8080;\r
+//osgi.conf("argeo.node.useradmin.uris", "os:///");\r
+//osgi.clean = true;\r
+osgi.launch();\r
diff --git a/dist/argeo-node/base/etc/argeo/argeo.ini b/dist/argeo-node/base/etc/argeo/argeo.ini
new file mode 100644 (file)
index 0000000..a995711
--- /dev/null
@@ -0,0 +1,32 @@
+## HTTP server
+org.osgi.service.http.port=8080
+
+## System management
+osgi.console=2323
+
+## Standalone
+#argeo.node.useradmin.uris=dc=example,dc=com.ldif
+#argeo.node.repo.type=h2
+
+## Deployed
+#argeo.node.useradmin.uris=ldap://cn=Directory%20Manager:argeoargeo@localhost/dc=example,dc=com
+#argeo.node.repo.type=postgresql
+#argeo.node.repo.dburl=jdbc:postgresql://localhost/argeo
+#argeo.node.repo.dbuser=argeo
+#argeo.node.repo.dbpassword=argeo
+
+## Complex user configuration examples
+#argeo.node.useradmin.uris="dc=example,dc=com.ldif dc=example,dc=org.ldif"
+#argeo.node.useradmin.uris="ldap://uid=admin,ou=system:secret@localhost:10389\
+#/dc=example,dc=com?userBase=ou=users&groupBase=ou=groups dc=example,dc=org.ldif"
+
+# Legacy
+#osgi.clean=true
+#java.security.manager=
+#java.security.policy=file:/usr/share/argeo/all.policy
+
+# Eclipse 3
+#argeo.osgi.start.4.eclipse3=\
+#org.eclipse.equinox.http.registry,\
+#org.eclipse.gemini.blueprint.extender
+#org.eclipse.rap.workbenchAutostart=false
diff --git a/dist/argeo-node/base/etc/argeo/conf.d/app-template.txt b/dist/argeo-node/base/etc/argeo/conf.d/app-template.txt
new file mode 100644 (file)
index 0000000..02aac6a
--- /dev/null
@@ -0,0 +1,7 @@
+# Rename to <my app>.ini
+
+# Backend
+#argeo.osgi.start.5.apps=org.argeo.suite.apps
+
+# UI
+#argeo.osgi.start.6.apps=org.argeo.suite.apps.web,org.argeo.suite.workbench.rap
diff --git a/dist/argeo-node/base/etc/argeo/log4j.properties b/dist/argeo-node/base/etc/argeo/log4j.properties
new file mode 100644 (file)
index 0000000..d93b234
--- /dev/null
@@ -0,0 +1,15 @@
+log4j.rootLogger=WARN, console
+
+log4j.logger.org.argeo=DEBUG
+
+## Appenders
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern=%-5p %m%n
+
+log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
+log4j.appender.file.File=/var/log/argeo/argeo.csv
+log4j.appender.file.layout=org.apache.log4j.PatternLayout
+log4j.appender.file.layout.ConversionPattern=%d{ISO8601};"%m";%c;%p%n
+log4j.appender.file.bufferedIO=true
+log4j.appender.file.immediateFlush=false
diff --git a/dist/argeo-node/base/etc/argeo/settings.sh b/dist/argeo-node/base/etc/argeo/settings.sh
new file mode 100644 (file)
index 0000000..8141f04
--- /dev/null
@@ -0,0 +1,3 @@
+export LANG=en_US.utf8
+JAVA_OPTS="-showversion -Xmx128m"
+#JAVA_OPTS="-showversion -Xmx512m -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=7084 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"
diff --git a/dist/argeo-node/base/share/argeo/SETUP.txt b/dist/argeo-node/base/share/argeo/SETUP.txt
new file mode 100644 (file)
index 0000000..41afce2
--- /dev/null
@@ -0,0 +1,8 @@
+
+# 389 Directory Server
+setup-ds.pl --silent --file=argeo-slapd.inf
+
+# PostgreSQL
+postgresql-setup initdb
+systemctl start postgresql
+sudo -u postgres psql < argeo-pgsql-setup.sql
diff --git a/dist/argeo-node/base/share/argeo/all.policy b/dist/argeo-node/base/share/argeo/all.policy
new file mode 100644 (file)
index 0000000..facb613
--- /dev/null
@@ -0,0 +1,3 @@
+grant {
+  permission java.security.AllPermission;
+};
\ No newline at end of file
diff --git a/dist/argeo-node/base/share/argeo/argeo-pgsql-setup.sql b/dist/argeo-node/base/share/argeo/argeo-pgsql-setup.sql
new file mode 100644 (file)
index 0000000..886f60a
--- /dev/null
@@ -0,0 +1,2 @@
+CREATE USER argeo WITH PASSWORD 'argeo';
+CREATE DATABASE argeo WITH OWNER argeo;
diff --git a/dist/argeo-node/base/share/argeo/argeo-slapd-setup.inf b/dist/argeo-node/base/share/argeo/argeo-slapd-setup.inf
new file mode 100644 (file)
index 0000000..6e37434
--- /dev/null
@@ -0,0 +1,12 @@
+[General] 
+FullMachineName=localhost.localdomain 
+SuiteSpotUserID=nobody 
+SuiteSpotGroup=nobody 
+
+[slapd] 
+ServerPort=389 
+ServerIdentifier=argeo 
+Suffix= dc=example,dc=com  
+RootDN= cn=Directory Manager 
+RootDNPwd=argeoargeo
+AddSampleEntries=Yes
diff --git a/dist/argeo-node/base/share/argeo/cms.js b/dist/argeo-node/base/share/argeo/cms.js
new file mode 100755 (executable)
index 0000000..7535377
--- /dev/null
@@ -0,0 +1,44 @@
+var System = Java.type("java.lang.System");
+var OsgiBuilder = Java.type("org.argeo.osgi.boot.OsgiBuilder");
+
+var osgi = new OsgiBuilder();
+// default bundles
+osgi.start(2, "org.eclipse.equinox.http.servlet");
+osgi.start(2, "org.eclipse.equinox.http.jetty");
+osgi.start(2, "org.eclipse.equinox.metatype");
+osgi.start(2, "org.eclipse.equinox.cm");
+osgi.start(2, "org.eclipse.equinox.ds");
+osgi.start(2, "org.eclipse.rap.rwt.osgi");
+osgi.start(3, "org.argeo.cms");
+osgi.start(4, "org.argeo.cms.e4.rap");
+// specific properties
+osgi.conf("org.eclipse.rap.workbenchAutostart", "false");
+osgi.conf("org.eclipse.equinox.http.jetty.autostart", "false");
+osgi.conf("org.osgi.framework.bootdelegation", "com.sun.jndi.ldap,"
+               + "com.sun.jndi.ldap.sasl," + "com.sun.security.jgss,"
+               + "com.sun.jndi.dns," + "com.sun.nio.file," + "com.sun.nio.sctp");
+
+var homeUri = java.nio.file.Paths
+               .get(java.lang.System.getProperty("user.home")).toUri().toString();
+var execDirUri = java.nio.file.Paths.get(
+               java.lang.System.getProperty("user.dir")).toUri().toString();
+if (typeof app !== 'undefined') {
+       if (typeof appHome == 'undefined') {
+               var appHome = homeUri + "/.a2/var/lib/" + app;
+       }
+       if (typeof appConf == 'undefined') {
+               var appConf = homeUri + "/.a2/etc/" + app;
+       }
+       if (typeof policyFile == 'undefined') {
+               var policyFile = "node.policy";
+       }
+       osgi.conf("osgi.configuration.area", appHome + "/state");
+       osgi.conf("osgi.instance.area", appHome + "/data");
+       // System.setProperty("java.security.manager", "");
+       // System.setProperty("java.security.policy", appConf + "/" + policyFile);
+       System.setProperty("log4j.configuration", appConf + "/log4j.properties");
+} else {
+       osgi.conf("osgi.configuration.area", execDirUri + "/state");
+       osgi.conf("osgi.instance.area", execDirUri + "/data");
+       System.setProperty("log4j.configuration", execDirUri + "etc/argeo/log4j.properties");
+}
diff --git a/dist/argeo-node/base/share/argeo/config.ini b/dist/argeo-node/base/share/argeo/config.ini
new file mode 100644 (file)
index 0000000..269fcf6
--- /dev/null
@@ -0,0 +1,37 @@
+# Only Argeo OSGi Boot is explicitly started
+osgi.bundles=org.argeo.osgi.boot@start
+
+# Required standard bundles to start
+argeo.osgi.start.2.node=\
+org.eclipse.equinox.http.servlet,\
+org.eclipse.equinox.http.jetty,\
+org.eclipse.equinox.metatype,\
+org.eclipse.equinox.cm,\
+org.eclipse.equinox.ds,\
+org.eclipse.rap.rwt.osgi
+
+# Required CMS bundles to start
+argeo.osgi.start.3.node=\
+org.argeo.cms
+
+# Extension managers
+argeo.osgi.start.4.node=\
+org.argeo.cms.e4.rap
+
+# Packages provided by the OpenJDK JVM
+org.osgi.framework.bootdelegation=com.sun.jndi.ldap,\
+com.sun.jndi.ldap.sasl,\
+com.sun.security.jgss,\
+com.sun.jndi.dns,\
+com.sun.nio.file,\
+com.sun.nio.sctp
+
+# Security manager
+#java.security.manager=
+#java.security.policy=file:/usr/share/node/all.policy
+
+# Required properties
+eclipse.ignoreApp=true
+osgi.noShutdown=true
+org.eclipse.equinox.http.jetty.autostart=false
+#org.eclipse.rap.workbenchAutostart=false
diff --git a/dist/argeo-node/pom.xml b/dist/argeo-node/pom.xml
new file mode 100644 (file)
index 0000000..5d2f581
--- /dev/null
@@ -0,0 +1,162 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>dist</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>argeo-node</artifactId>
+       <packaging>pom</packaging>
+       <name>Argeo Node</name>
+       <profiles>
+               <profile>
+                       <id>dist</id>
+                       <dependencies>
+                               <dependency>
+                                       <groupId>org.argeo.commons</groupId>
+                                       <artifactId>org.argeo.dep.cms.e4.rap</artifactId>
+                                       <version>2.1.76-SNAPSHOT</version>
+                               </dependency>
+                               <dependency>
+                                       <groupId>org.argeo.commons</groupId>
+                                       <artifactId>osgi-boot</artifactId>
+                                       <type>zip</type>
+                                       <version>2.1.76-SNAPSHOT</version>
+                               </dependency>
+                       </dependencies>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <groupId>org.apache.maven.plugins</groupId>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <configuration>
+                                                       <finalName>argeo-node-${project.version}</finalName>
+                                                       <appendAssemblyId>false</appendAssemblyId>
+                                                       <descriptors>
+                                                               <descriptor>assembly/cms-e4-rap.xml</descriptor>
+                                                       </descriptors>
+                                               </configuration>
+                                               <executions>
+                                                       <execution>
+                                                               <id>assembly-base</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+               <profile>
+                       <id>rpmbuild</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-node</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-node</name>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/etc/argeo</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>argeo</groupname>
+                                                                                       <filemode>640</filemode>
+                                                                                       <configuration>noreplace</configuration>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>base/etc/argeo</location>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <mapping>
+                                                                                       <directory>/etc/argeo/conf.d</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>argeo</groupname>
+                                                                                       <filemode>640</filemode>
+                                                                                       <configuration>noreplace</configuration>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>base/etc/argeo/conf.d</location>
+                                                                                                       <includes>
+                                                                                                               <include>*.ini</include>
+                                                                                                               <include>*.txt</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/argeo</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>base/share/argeo</location>
+                                                                                                       <includes>
+                                                                                                               <include>**</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <mapping>
+                                                                                       <directory>/usr/lib/systemd/system</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>rpm/usr/lib/systemd/system</location>
+                                                                                                       <includes>
+                                                                                                               <include>*.service</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <mapping>
+                                                                                       <directory>/usr/sbin</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>755</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>rpm/usr/sbin</location>
+                                                                                                       <includes>
+                                                                                                               <include>argeoctl</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                       </mappings>
+                                                                       <preinstallScriptlet>
+                                                                               <scriptFile>rpm/scripts/preinstall</scriptFile>
+                                                                       </preinstallScriptlet>
+                                                                       <requires>
+                                                                               <require>argeo-cms-node</require>
+                                                                               <require>osgi-boot</require>
+                                                                               <!-- do not explicitely require java -->
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
+</project>
diff --git a/dist/argeo-node/rpm/etc/node/conf.d/app-template.txt b/dist/argeo-node/rpm/etc/node/conf.d/app-template.txt
new file mode 100644 (file)
index 0000000..02aac6a
--- /dev/null
@@ -0,0 +1,7 @@
+# Rename to <my app>.ini
+
+# Backend
+#argeo.osgi.start.5.apps=org.argeo.suite.apps
+
+# UI
+#argeo.osgi.start.6.apps=org.argeo.suite.apps.web,org.argeo.suite.workbench.rap
diff --git a/dist/argeo-node/rpm/etc/node/log4j.properties b/dist/argeo-node/rpm/etc/node/log4j.properties
new file mode 100644 (file)
index 0000000..d53b851
--- /dev/null
@@ -0,0 +1,15 @@
+log4j.rootLogger=WARN, console, file
+
+log4j.logger.org.argeo=INFO
+
+## Appenders
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern=%-5p %m%n
+
+log4j.appender.file=org.apache.log4j.DailyRollingFileAppender
+log4j.appender.file.File=/var/log/node/node.csv
+log4j.appender.file.layout=org.apache.log4j.PatternLayout
+log4j.appender.file.layout.ConversionPattern=%d{ISO8601};"%m";%c;%p%n
+log4j.appender.file.bufferedIO=true
+log4j.appender.file.immediateFlush=false
diff --git a/dist/argeo-node/rpm/etc/node/node.ini b/dist/argeo-node/rpm/etc/node/node.ini
new file mode 100644 (file)
index 0000000..37cf1b4
--- /dev/null
@@ -0,0 +1,29 @@
+## Provisioning
+#osgi.clean=true
+#argeo.osgi.bundles=\
+#/etc/node/modules;in=*/*,\
+#/usr/local/share/osgi;in=**/*.jar,\
+#/usr/share/osgi;in=**/*.jar;ex=boot/*.jar
+
+## HTTP server
+org.osgi.service.http.port=8080
+
+## System management
+osgi.console=2323
+
+## Standalone
+#argeo.node.useradmin.uris=dc=example,dc=com.ldif
+#argeo.node.repo.type=h2
+
+## Deployed
+#argeo.node.useradmin.uris=ldap://cn=Directory%20Manager:argeoargeo@localhost/dc=example,dc=com
+#argeo.node.repo.type=postgresql
+#argeo.node.repo.dburl=jdbc:postgresql://localhost/node
+#argeo.node.repo.dbuser=argeo
+#argeo.node.repo.dbpassword=argeo
+
+## Complex user configuration examples
+#argeo.node.useradmin.uris="dc=example,dc=com.ldif dc=example,dc=org.ldif"
+#argeo.node.useradmin.uris="ldap://uid=admin,ou=system:secret@localhost:10389\
+#/dc=example,dc=com?userBase=ou=users&groupBase=ou=groups dc=example,dc=org.ldif"
+
diff --git a/dist/argeo-node/rpm/etc/node/settings.sh b/dist/argeo-node/rpm/etc/node/settings.sh
new file mode 100644 (file)
index 0000000..8141f04
--- /dev/null
@@ -0,0 +1,3 @@
+export LANG=en_US.utf8
+JAVA_OPTS="-showversion -Xmx128m"
+#JAVA_OPTS="-showversion -Xmx512m -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=7084 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"
diff --git a/dist/argeo-node/rpm/scripts/preinstall b/dist/argeo-node/rpm/scripts/preinstall
new file mode 100644 (file)
index 0000000..2e46065
--- /dev/null
@@ -0,0 +1,15 @@
+if [ $1 = "1" ];then
+       APP=argeo
+       
+       # check if user exists
+       /bin/id $APP 2>/dev/null
+       if [ $? -ne 0 ];then
+               echo Create user $APP...
+               useradd --system --home-dir /var/lib/$APP --shell /bin/bash --user-group --create-home $APP
+       else
+               echo User $APP already exists
+       fi
+       
+       mkdir -p /var/lib/$APP/{state,data,indexes}
+       chown -R $APP.$APP /var/lib/$APP
+fi
\ No newline at end of file
diff --git a/dist/argeo-node/rpm/usr/lib/systemd/system/argeo.service b/dist/argeo-node/rpm/usr/lib/systemd/system/argeo.service
new file mode 100644 (file)
index 0000000..f30e9fb
--- /dev/null
@@ -0,0 +1,14 @@
+[Unit]
+Description=Argeo Node
+After=network.target
+Wants=postgresql.service
+
+[Service]
+Type=simple
+PIDFile=/var/run/argeo/argeo.pid
+ExecStart=/usr/sbin/argeoctl start
+ExecReload=/usr/sbin/argeoctl reload
+SuccessExitStatus=143
+
+[Install]
+WantedBy=multi-user.target
diff --git a/dist/argeo-node/rpm/usr/sbin/argeoctl b/dist/argeo-node/rpm/usr/sbin/argeoctl
new file mode 100755 (executable)
index 0000000..ccadd8e
--- /dev/null
@@ -0,0 +1,129 @@
+#!/bin/sh
+APP=argeo
+
+JVM=java
+
+# Directories and files
+CONF_DIR=/etc/$APP
+CONF_DIRS=/etc/$APP/conf.d
+BASE_POLICY_ALL=/usr/share/$APP/all.policy
+BASE_CONFIG_INI=/usr/share/$APP/config.ini
+
+EXEC_DIR=/var/lib/$APP
+DATA_DIR=$EXEC_DIR/data
+CONF_RW=$EXEC_DIR/state
+CONFIG_INI=$CONF_RW/config.ini
+
+OSGI_INSTALL_AREA=/usr/share/osgi/boot
+OSGI_FRAMEWORK=$OSGI_INSTALL_AREA/org.eclipse.osgi.jar
+
+# Overwrite variables
+if [ -f $CONF_DIR/settings.sh ];then
+       . $CONF_DIR/settings.sh
+fi
+
+RETVAL=0
+
+start() {
+       mkdir -p $CONF_RW
+       mkdir -p $DATA_DIR
+
+    # Merge config files
+    printf "## Equinox configuration - Generated by /usr/sbin/nodectl ##\n\n" > $CONFIG_INI
+    cat $BASE_CONFIG_INI >> $CONFIG_INI
+    printf "\n##\n## $CONF_DIR/$APP.ini\n##\n\n" >> $CONFIG_INI
+    cat $CONF_DIR/$APP.ini >> $CONFIG_INI
+    for file in `ls -v $CONF_DIRS/*.ini`; do
+            printf "\n##\n## $file\n##\n\n" >> $CONFIG_INI
+            cat $file >> $CONFIG_INI
+    done;
+
+       cd $EXEC_DIR
+       $JVM \
+               -Dlog4j.configuration="file:$CONF_DIR/log4j.properties" \
+               $JAVA_OPTS -jar $OSGI_FRAMEWORK \
+               -configuration "$CONF_RW" \
+               -data "$DATA_DIR"
+}
+
+reload() {
+       echo Not yet implemented
+}
+
+stop() {
+       if [ -f $PID_FILE ];then
+               PID=`cat $PID_FILE`
+               kill -0 $PID &> /dev/null
+               PID_EXISTS=$?
+               if [ $PID_EXISTS -ne 0 ]; then
+                       echo Dead $APP process with pid $PID, removing $PID_FILE
+                       rm -f $PID_FILE
+                       RETVAL=1
+                       return $RETVAL
+               fi
+       else
+               echo $APP is not running
+               RETVAL=1
+               return $RETVAL
+       fi
+       
+       # notifies application by removing the shutdown file
+#      rm -f $SHUTDOWN_FILE
+       kill $PID
+       
+       # wait 10 min for application to shutdown, then kill it
+       TIMEOUT=$((10*60))
+       BEGIN=$(date +%s)
+       while kill -0 $PID &> /dev/null
+       do
+               sleep 1
+               NOW=$(date +%s)
+               DURATION=$(($NOW-$BEGIN))
+               if [ $DURATION -gt $TIMEOUT ]; then
+                       kill -9 $PID
+                       echo Forcibly killed $APP with pid $PID
+                       RETVAL=1
+               fi
+       done
+       
+       # remove pid file
+       rm -f $PID_FILE
+       return $RETVAL
+}
+
+status() {
+       if [ -f $PID_FILE ];then
+               PID=`cat $PID_FILE`
+       else
+               echo $APP is not running
+               return $RETVAL
+       fi
+       kill -0 $PID &> /dev/null
+       PID_EXISTS=$?
+       if [ $PID_EXISTS -eq 0 ]; then
+               echo $APP is running with pid $PID ...
+       else
+               echo No $APP process with pid $PID, removing $PID_FILE
+               rm -f $PID_FILE
+       fi
+       return $RETVAL
+}
+
+# main
+case "$1" in
+  start)
+        start
+        ;;
+  reload)
+        reload
+        ;;
+  stop)
+        stop
+        ;;
+  status)
+       status
+        ;;
+  *)
+        echo $"Usage: $0 {start|stop|status}"
+        exit 1
+esac
\ No newline at end of file
diff --git a/dist/argeo-node/rpm/usr/share/node/all.policy b/dist/argeo-node/rpm/usr/share/node/all.policy
new file mode 100644 (file)
index 0000000..facb613
--- /dev/null
@@ -0,0 +1,3 @@
+grant {
+  permission java.security.AllPermission;
+};
\ No newline at end of file
diff --git a/dist/argeo-node/rpm/usr/share/node/config.ini b/dist/argeo-node/rpm/usr/share/node/config.ini
new file mode 100644 (file)
index 0000000..ae7a664
--- /dev/null
@@ -0,0 +1,38 @@
+# Only Argeo OSGi Boot is explicitly started
+osgi.bundles=org.argeo.osgi.boot@start
+
+# Required standard bundles to start
+argeo.osgi.start.2.node=\
+org.eclipse.equinox.http.servlet,\
+org.eclipse.equinox.http.jetty,\
+org.eclipse.equinox.metatype,\
+org.eclipse.equinox.cm,\
+org.eclipse.equinox.ds,\
+org.eclipse.rap.rwt.osgi
+
+# Required CMS bundles to start
+argeo.osgi.start.3.node=\
+org.argeo.cms
+
+# Extension managers
+argeo.osgi.start.4.node=\
+org.eclipse.gemini.blueprint.extender,\
+org.argeo.cms.e4.rap
+
+# Packages provided by the OpenJDK JVM
+org.osgi.framework.bootdelegation=com.sun.jndi.ldap,\
+com.sun.jndi.ldap.sasl,\
+com.sun.security.jgss,\
+com.sun.jndi.dns,\
+com.sun.nio.file,\
+com.sun.nio.sctp
+
+# Security manager
+java.security.manager=
+java.security.policy=file:/usr/share/node/all.policy
+
+# Required properties
+eclipse.ignoreApp=true
+osgi.noShutdown=true
+org.eclipse.equinox.http.jetty.autostart=false
+org.eclipse.rap.workbenchAutostart=false
diff --git a/dist/argeo-node/rpm/usr/share/node/jjs/cms.js b/dist/argeo-node/rpm/usr/share/node/jjs/cms.js
new file mode 100755 (executable)
index 0000000..446747f
--- /dev/null
@@ -0,0 +1,46 @@
+var System = Java.type("java.lang.System");
+var OsgiBuilder = Java.type("org.argeo.osgi.boot.OsgiBuilder");
+
+var osgi = new OsgiBuilder();
+// default bundles
+osgi.start(2, "org.eclipse.equinox.http.servlet");
+osgi.start(2, "org.eclipse.equinox.http.jetty");
+osgi.start(2, "org.eclipse.equinox.metatype");
+osgi.start(2, "org.eclipse.equinox.cm");
+osgi.start(2, "org.eclipse.rap.rwt.osgi");
+osgi.start(3, "org.argeo.cms");
+osgi.start(4, "org.eclipse.gemini.blueprint.extender");
+osgi.start(4, "org.eclipse.equinox.http.registry");
+// specific properties
+osgi.conf("org.eclipse.rap.workbenchAutostart", "false");
+osgi.conf("org.eclipse.equinox.http.jetty.autostart", "false");
+osgi.conf("org.osgi.framework.bootdelegation", "com.sun.jndi.ldap,"
+               + "com.sun.jndi.ldap.sasl," + "com.sun.security.jgss,"
+               + "com.sun.jndi.dns," + "com.sun.nio.file," + "com.sun.nio.sctp");
+
+var homeUri = java.nio.file.Paths
+               .get(java.lang.System.getProperty("user.home")).toUri().toString();
+if (typeof app !== 'undefined') {
+       if (typeof appHome == 'undefined') {
+               var appHome = homeUri + "/.a2/var/lib/" + app;
+       }
+       if (typeof appConf == 'undefined') {
+               var appConf = homeUri + "/.a2/etc/" + app;
+       }
+       if (typeof policyFile == 'undefined') {
+               var policyFile = "node.policy";
+       }
+       osgi.conf("osgi.configuration.area", appHome + "/state");
+       osgi.conf("osgi.instance.area", appHome + "/data");
+       System.setProperty("java.security.manager", "");
+       System.setProperty("java.security.policy", appConf + "/" + policyFile);
+       System.setProperty("log4j.configuration", appConf + "/log4j.properties");
+}
+
+function openWorkbench() {
+       osgi.spring("org.argeo.cms.ui.workbench.rap");
+       var appUrl = "http://127.0.0.1:" + osgi.httpPort + "/ui/node";
+       $EXEC("chrome --app=" + appUrl);
+       // shutdown when the window is closed
+       osgi.shutdown();
+}
\ No newline at end of file
diff --git a/dist/osgi-boot/.gitignore b/dist/osgi-boot/.gitignore
new file mode 100644 (file)
index 0000000..66cc710
--- /dev/null
@@ -0,0 +1,2 @@
+/target/
+
diff --git a/dist/osgi-boot/assembly/osgi-boot.xml b/dist/osgi-boot/assembly/osgi-boot.xml
new file mode 100644 (file)
index 0000000..adad22b
--- /dev/null
@@ -0,0 +1,34 @@
+<assembly
+       xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+       <id>dist</id>
+       <baseDirectory></baseDirectory>
+       <formats>
+               <format>zip</format>
+       </formats>
+       <fileSets>
+               <fileSet>
+                       <directory>base/bin</directory>
+                       <outputDirectory>bin</outputDirectory>
+                       <fileMode>0755</fileMode>
+                       <includes>
+                               <include>*</include>
+                       </includes>
+                       <excludes>
+                               <exclude>offline.sh</exclude>
+                       </excludes>
+               </fileSet>
+       </fileSets>
+       <dependencySets>
+               <dependencySet>
+                       <unpack>false</unpack>
+                       <outputFileNameMapping>${artifact.artifactId}.${artifact.extension}</outputFileNameMapping>
+                       <outputDirectory>share/osgi/boot</outputDirectory>
+                       <includes>
+                               <include>org.argeo.tp.equinox:org.eclipse.osgi</include>
+                               <include>org.argeo.commons:org.argeo.osgi.boot</include>
+                       </includes>
+               </dependencySet>
+       </dependencySets>
+</assembly>
\ No newline at end of file
diff --git a/dist/osgi-boot/base/bin/a2jjs b/dist/osgi-boot/base/bin/a2jjs
new file mode 100755 (executable)
index 0000000..128bcea
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/sh
+BIN_DIR=`dirname $0`
+EQUINOX=$BIN_DIR/../share/osgi/boot/org.eclipse.osgi.jar
+OSGI_BOOT=$BIN_DIR/../share/osgi/boot/org.argeo.osgi.boot.jar
+
+/usr/bin/jjs -cp "$EQUINOX:$OSGI_BOOT" $*
diff --git a/dist/osgi-boot/pom.xml b/dist/osgi-boot/pom.xml
new file mode 100644 (file)
index 0000000..41270c3
--- /dev/null
@@ -0,0 +1,216 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>dist</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>osgi-boot</artifactId>
+       <packaging>pom</packaging>
+       <name>Commons Deployable OSGi Boot</name>
+       <properties>
+               <version.equinox>3.10.1.v20140909-1633</version.equinox>
+       </properties>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.tp</groupId>
+                       <artifactId>argeo-tp</artifactId>
+                       <version>${version.argeo-tp}</version>
+               </dependency>
+
+               <!-- OSGi Boot (and Equinox) -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.osgi.boot</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+       </dependencies>
+       <profiles>
+               <profile>
+                       <id>dist</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <groupId>org.apache.maven.plugins</groupId>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <configuration>
+                                                       <finalName>osgi-boot-${project.version}</finalName>
+                                                       <appendAssemblyId>false</appendAssemblyId>
+                                                       <descriptors>
+                                                               <descriptor>assembly/osgi-boot.xml</descriptor>
+                                                       </descriptors>
+                                               </configuration>
+                                               <executions>
+                                                       <execution>
+                                                               <id>assembly-base</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+               <profile>
+                       <id>rpmbuild</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-osgi-boot</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>osgi-boot</name>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/etc/osgiboot</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <configuration>noreplace</configuration>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>src/main/rpm/etc/osgiboot</location>
+                                                                                                       <includes>
+                                                                                                               <include>*-settings.sh</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <mapping>
+                                                                                       <directory>/etc/osgiboot</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>src/main/rpm/etc/osgiboot</location>
+                                                                                                       <includes>
+                                                                                                               <include>*.policy</include>
+                                                                                                               <include>*-functions.sh</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <mapping>
+                                                                                       <directory>/usr/bin</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>755</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>src/main/rpm/usr/sbin</location>
+                                                                                                       <includes>
+                                                                                                               <include>*</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <mapping>
+                                                                                       <directory>/usr/sbin</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>755</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>src/main/rpm/usr/sbin</location>
+                                                                                                       <includes>
+                                                                                                               <include>osgi-service</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi/boot</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <dependency>
+                                                                                               <stripVersion>true</stripVersion>
+                                                                                               <includes>
+                                                                                                       <include>org.argeo.commons:org.argeo.osgi.boot</include>
+                                                                                               </includes>
+                                                                                       </dependency>
+                                                                                       <!-- <sources> -->
+                                                                                       <!-- <source> -->
+                                                                                       <!-- <location>${project.build.directory}/lib</location> -->
+                                                                                       <!-- <includes> -->
+                                                                                       <!-- <include>org.argeo.osgi.boot.jar</include> -->
+                                                                                       <!-- </includes> -->
+                                                                                       <!-- </source> -->
+                                                                                       <!-- </sources> -->
+                                                                               </mapping>
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>osgi-boot-equinox</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+               <profile>
+                       <id>rpmbuild-tp</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-osgi-boot-equinox</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>osgi-boot-equinox</name>
+                                                                       <projversion>${version.argeo-tp}</projversion>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi/boot</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <dependency>
+                                                                                               <stripVersion>true</stripVersion>
+                                                                                               <includes>
+                                                                                                       <include>org.argeo.tp.equinox:org.eclipse.osgi</include>
+                                                                                               </includes>
+                                                                                       </dependency>
+                                                                                       <!-- <sources> -->
+                                                                                       <!-- <source> -->
+                                                                                       <!-- <location>${project.build.directory}/lib</location> -->
+                                                                                       <!-- <includes> -->
+                                                                                       <!-- <include>org.eclipse.osgi.jar</include> -->
+                                                                                       <!-- </includes> -->
+                                                                                       <!-- </source> -->
+                                                                                       <!-- </sources> -->
+                                                                               </mapping>
+                                                                       </mappings>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
+</project>
diff --git a/dist/osgi-boot/src/main/rpm/etc/osgiboot/all.policy b/dist/osgi-boot/src/main/rpm/etc/osgiboot/all.policy
new file mode 100644 (file)
index 0000000..facb613
--- /dev/null
@@ -0,0 +1,3 @@
+grant {
+  permission java.security.AllPermission;
+};
\ No newline at end of file
diff --git a/dist/osgi-boot/src/main/rpm/etc/osgiboot/osgi-service-init-functions.sh b/dist/osgi-boot/src/main/rpm/etc/osgiboot/osgi-service-init-functions.sh
new file mode 100644 (file)
index 0000000..ba23519
--- /dev/null
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+# Source function library.
+. /etc/rc.d/init.d/functions
+
+RETVAL=0
+
+osgi_service_start() {
+       APP=$1
+       # create log and run directories writable by the application user
+       USER=$APP
+       GROUP=$APP
+       RUN_DIR=/var/run/$APP
+       LOG_DIR=/var/log/$APP
+       if [ ! -d $LOG_DIR ];then
+               mkdir -m 0750 $LOG_DIR
+               chown -R $USER.$GROUP $LOG_DIR
+       fi
+       if [ ! -d $RUN_DIR ];then
+               mkdir -m 0750 $RUN_DIR
+               chown -R $USER.$GROUP $RUN_DIR
+       fi
+       
+       # call Argeo Commons OSGi utilities as the application user
+       daemon --user $USER /usr/sbin/osgi-service $APP start
+       
+       RETVAL=$?
+       #action $"Start $APP" /bin/true
+       if [ $RETVAL -eq 0 ];then
+               PID=`cat $RUN_DIR/$APP.pid`
+               action $"Started $APP with pid $PID" /bin/true
+       else
+               action $"Could not start $APP" /bin/false
+       fi
+       return $RETVAL
+}
+
+osgi_service_stop() {
+       APP=$1
+       USER=$APP
+       # call Argeo Commons OSGi utilities as the application user
+       runuser -s /bin/bash $USER -c "/usr/sbin/osgi-service $APP stop"
+       RETVAL=$?
+       if [ $RETVAL -eq 0 ];then
+               action $"Stopped $APP" /bin/true
+       else
+               action $"Could not stop $APP" /bin/false
+       fi
+       return $RETVAL
+}
+
+osgi_service_status() {
+       APP=$1
+       USER=$APP
+       # call Argeo Commons OSGi utilities as the application user
+       runuser -s /bin/bash $USER -c "/usr/sbin/osgi-service $APP status"
+       RETVAL=$?
+       return $RETVAL
+}
diff --git a/dist/osgi-boot/src/main/rpm/etc/osgiboot/osgi-service-settings.sh b/dist/osgi-boot/src/main/rpm/etc/osgiboot/osgi-service-settings.sh
new file mode 100644 (file)
index 0000000..f5504f8
--- /dev/null
@@ -0,0 +1 @@
+#JAVA_OPTS=-Xmx256m
\ No newline at end of file
diff --git a/dist/osgi-boot/src/main/rpm/usr/bin/a2jjs b/dist/osgi-boot/src/main/rpm/usr/bin/a2jjs
new file mode 100755 (executable)
index 0000000..62762a8
--- /dev/null
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+export A2_HOME=$HOME/.a2
+if [ -d "$A2_HOME/share/osgi/boot" ]; then
+       PREFIX=$A2_HOME
+else
+       PREFIX=/usr
+fi
+
+EQUINOX=$PREFIX/share/osgi/boot/org.eclipse.osgi.jar
+OSGI_BOOT=$PREFIX/share/osgi/boot/org.argeo.osgi.boot.jar
+
+/usr/bin/jjs -cp "$EQUINOX:$OSGI_BOOT" $*
diff --git a/dist/osgi-boot/src/main/rpm/usr/sbin/osgi-service b/dist/osgi-boot/src/main/rpm/usr/sbin/osgi-service
new file mode 100644 (file)
index 0000000..2ce1b61
--- /dev/null
@@ -0,0 +1,160 @@
+#!/bin/sh
+
+JVM=java
+. /etc/osgiboot/osgi-service-settings.sh
+
+APP=$1
+
+CONF_DIR=/etc/$APP
+if [ -f $CONF_DIR/settings.sh ];then
+       . $CONF_DIR/settings.sh
+fi
+
+LIB_DIR=/usr/share/$APP/lib
+
+# read/write
+EXEC_DIR=/var/lib/$APP
+DATA_DIR=$EXEC_DIR/data
+CONF_RW=$EXEC_DIR/conf
+
+LOG_DIR=/var/log/$APP
+LOG_FILE=$LOG_DIR/$APP.log
+
+RUN_DIR=/var/run/$APP
+PID_FILE=$RUN_DIR/$APP.pid
+SHUTDOWN_FILE=$RUN_DIR/$APP.shutdown
+
+OSGI_INSTALL_AREA=/usr/share/osgi/boot
+OSGI_FRAMEWORK=$OSGI_INSTALL_AREA/org.eclipse.osgi.jar
+
+RETVAL=0
+
+start() {
+       if [ -f $PID_FILE ];then
+               PID=`cat $PID_FILE`
+               kill -0 $PID &> /dev/null
+               PID_EXISTS=$?
+               if [ $PID_EXISTS -eq 0 ]; then
+                       echo $APP already running with pid $PID
+                       RETVAL=1
+                       return $RETVAL
+               else
+                       echo Old $APP process with pid $PID is dead, removing $PID_FILE
+                       rm -f $PID_FILE
+               fi
+       fi
+
+       if [ ! -f $CONF_RW/config.ini ]; then
+               #echo osgi.configuration.cascaded=true > $CONF_RW/config.ini
+               #echo osgi.sharedConfiguration.area=$CONF_DIR >> $CONF_RW/config.ini
+               #echo osgi.sharedConfiguration.area.readOnly=true >> $CONF_RW/config.ini
+               cp --preserve $CONF_DIR/config.ini $CONF_RW/config.ini
+       fi
+       touch $SHUTDOWN_FILE
+       cd $EXEC_DIR
+       $JVM \
+               -Dargeo.osgi.shutdownFile="$SHUTDOWN_FILE" \
+               -Dlog4j.configuration="file:$CONF_DIR/log4j.properties" \
+               -Djava.security.manager= \
+               -Djava.security.policy="file:/etc/osgiboot/all.policy" \
+               $JAVA_OPTS -jar $OSGI_FRAMEWORK \
+               -clean \
+               -configuration "$CONF_RW" \
+               -data "$DATA_DIR" \
+               >> $LOG_FILE 2>&1 &
+       # (above) stderr redirected to stdout, then stdout to log file
+       # see http://tldp.org/LDP/abs/html/io-redirection.html
+       PID=$!
+       echo $PID > $PID_FILE
+       #echo Started $APP with pid $PID
+       return $RETVAL
+}
+
+stop() {
+       if [ -f $PID_FILE ];then
+               PID=`cat $PID_FILE`
+               kill -0 $PID &> /dev/null
+               PID_EXISTS=$?
+               if [ $PID_EXISTS -ne 0 ]; then
+                       echo Dead $APP process with pid $PID, removing $PID_FILE
+                       rm -f $PID_FILE
+                       RETVAL=1
+                       return $RETVAL
+               fi
+       else
+               echo $APP is not running
+               RETVAL=1
+               return $RETVAL
+       fi
+       
+       # notifies application by removing the shutdown file
+       rm -f $SHUTDOWN_FILE
+       
+       # wait 5 min for application to shutdown, then kill it
+       TIMEOUT=$((5*60))
+       BEGIN=$(date +%s)
+       while kill -0 $PID &> /dev/null
+       do
+               sleep 1
+               NOW=$(date +%s)
+               DURATION=$(($NOW-$BEGIN))
+               if [ $DURATION -gt $TIMEOUT ]; then
+                       kill -9 $PID
+                       echo Forcibly killed $APP with pid $PID
+                       RETVAL=1
+               fi
+       done
+       
+       # remove pid file
+       rm -f $PID_FILE
+       return $RETVAL
+
+# timeout is only available in EL6
+#      timeout 5m sh << EOF
+#while kill -0 $PID &> /dev/null; do sleep 1; done
+#EOF
+#      TIMEOUT_EXIT=$?
+#      if [ $TIMEOUT_EXIT -eq 124 ];then
+#              kill -9 $PID
+#              RETVAL=1
+#              echo Killed $APP with pid $PID
+#      else
+#              echo Stopped $APP with pid $PID
+#      fi
+#      rm -f $PID_FILE
+#      return $RETVAL
+}
+
+status() {
+       if [ -f $PID_FILE ];then
+               PID=`cat $PID_FILE`
+       else
+               echo $APP is not running
+               return $RETVAL
+       fi
+       kill -0 $PID &> /dev/null
+       PID_EXISTS=$?
+       if [ $PID_EXISTS -eq 0 ]; then
+               echo $APP is running with pid $PID ...
+       else
+               echo No $APP process with pid $PID, removing $PID_FILE
+               rm -f $PID_FILE
+       fi
+       return $RETVAL
+}
+
+# main
+case "$2" in
+  start)
+        start
+        ;;
+  stop)
+        stop
+        ;;
+  status)
+       status
+        ;;
+  *)
+        echo $"Usage: $0 {start|stop|status}"
+        exit 1
+esac
\ No newline at end of file
diff --git a/dist/pom.xml b/dist/pom.xml
new file mode 100644 (file)
index 0000000..e520e83
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>dist</artifactId>
+       <name>Commons Deployable Distributions</name>
+       <packaging>pom</packaging>
+       <modules>
+               <module>osgi-boot</module>
+               <module>argeo-node</module>
+       </modules>
+</project>
\ No newline at end of file
diff --git a/doc/.gitignore b/doc/.gitignore
new file mode 100644 (file)
index 0000000..ea8c4bf
--- /dev/null
@@ -0,0 +1 @@
+/target
diff --git a/doc/docbook/argeo-commons.dbk.xml b/doc/docbook/argeo-commons.dbk.xml
new file mode 100644 (file)
index 0000000..2ddbfa4
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook V5.0/EN" "http://docbook.org/xml/5.0/dtd/docbook.dtd" [
+<!ENTITY mbaudier "Mathieu Baudier">
+]>
+<book xmlns="http://docbook.org/ns/docbook" version="5.0">
+       <xi:include href="overview.dbk.xml" xmlns:xi="http://www.w3.org/2001/XInclude" />
+       <xi:include href="deploying.dbk.xml" xmlns:xi="http://www.w3.org/2001/XInclude" />
+</book>
\ No newline at end of file
diff --git a/doc/docbook/argeo.css b/doc/docbook/argeo.css
new file mode 100644 (file)
index 0000000..c9c6195
--- /dev/null
@@ -0,0 +1,11 @@
+body {
+       font-family: sans-serif;
+}
+
+h1 {
+       font-size: 150%
+}
+
+h2 {
+       font-size: 130%
+}
\ No newline at end of file
diff --git a/doc/docbook/deploying.dbk.xml b/doc/docbook/deploying.dbk.xml
new file mode 100644 (file)
index 0000000..0e1fe0c
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook V5.0/EN" "http://docbook.org/xml/5.0/dtd/docbook.dtd">
+<chapter xml:id="deploying">
+       <title>Deploying</title>
+       <section>
+               <title>On CentOS or Red Hat Enterprise Linux</title>
+               <para></para>
+       </section>
+</chapter>
\ No newline at end of file
diff --git a/doc/docbook/overview.dbk.xml b/doc/docbook/overview.dbk.xml
new file mode 100644 (file)
index 0000000..72690ae
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook V5.0/EN" "http://docbook.org/xml/5.0/dtd/docbook.dtd">
+<chapter xml:id="overview">
+       <title>Overview</title>
+       <para>Argeo Commons is an integration framework.</para>
+       <section>
+               <title>Argeo Commons within the Argeo software stack</title>
+               <para></para>
+       </section>
+</chapter>
\ No newline at end of file
diff --git a/doc/pom.xml b/doc/pom.xml
new file mode 100644 (file)
index 0000000..8564a74
--- /dev/null
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>doc</artifactId>
+       <name>Commons Documentation</name>
+       <packaging>pom</packaging>
+       <build>
+               <plugins>
+                       <plugin>
+                               <groupId>com.agilejava.docbkx</groupId>
+                               <artifactId>docbkx-maven-plugin</artifactId>
+                               <version>2.0.15</version>
+                               <configuration>
+                                       <sourceDirectory>docbook</sourceDirectory>
+                                       <includes>argeo-commons.dbk.xml</includes>
+                                       <htmlStylesheet>argeo.css</htmlStylesheet>
+                                       <xincludeSupported>true</xincludeSupported>
+                                       <preProcess>
+                                               <copy todir="${project.build.directory}/docbkx/xhtml5" file="docbook/argeo.css" />
+                                       </preProcess>
+                               </configuration>
+                               <dependencies>
+                                       <dependency>
+                                               <groupId>net.sf.docbook</groupId>
+                                               <artifactId>docbook-xml</artifactId>
+                                               <version>5.0-all</version>
+                                               <classifier>resources</classifier>
+                                               <type>zip</type>
+                                               <scope>runtime</scope>
+                                       </dependency>
+                               </dependencies>
+                               <executions>
+                                       <execution>
+                                               <goals>
+                                                       <goal>generate-xhtml5</goal>
+                                               </goals>
+                                               <phase>package</phase>
+                                       </execution>
+                               </executions>
+                       </plugin>
+               </plugins>
+       </build>
+       <repositories>
+               <!-- <repository> -->
+               <!-- <id>jboss</id> -->
+               <!-- <url>http://repository.jboss.org/</url> -->
+               <!-- <releases> -->
+               <!-- <enabled>true</enabled> -->
+               <!-- </releases> -->
+               <!-- <snapshots> -->
+               <!-- <enabled>false</enabled> -->
+               <!-- </snapshots> -->
+               <!-- </repository> -->
+               <repository>
+                       <id>central</id>
+                       <url>http://repo1.maven.org/maven2</url>
+                       <releases>
+                               <enabled>true</enabled>
+                       </releases>
+                       <snapshots>
+                               <enabled>false</enabled>
+                       </snapshots>
+               </repository>
+       </repositories>
+       <pluginRepositories>
+               <!-- <pluginRepository> -->
+               <!-- <id>jboss</id> -->
+               <!-- <url>http://repository.jboss.org/</url> -->
+               <!-- </pluginRepository> -->
+       </pluginRepositories>
+</project>
\ No newline at end of file
diff --git a/license-apache2-header.txt b/license-apache2-header.txt
new file mode 100644 (file)
index 0000000..be033d0
--- /dev/null
@@ -0,0 +1,13 @@
+Copyright (C) 2007-2012 Argeo GmbH
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/maven/.gitignore b/maven/.gitignore
new file mode 100644 (file)
index 0000000..b83d222
--- /dev/null
@@ -0,0 +1 @@
+/target/
diff --git a/maven/assembly-descriptors/.gitignore b/maven/assembly-descriptors/.gitignore
new file mode 100644 (file)
index 0000000..b83d222
--- /dev/null
@@ -0,0 +1 @@
+/target/
diff --git a/maven/assembly-descriptors/META-INF/.gitignore b/maven/assembly-descriptors/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/maven/assembly-descriptors/assemblies/a2-source-tp.xml b/maven/assembly-descriptors/assemblies/a2-source-tp.xml
new file mode 100644 (file)
index 0000000..99423f7
--- /dev/null
@@ -0,0 +1,22 @@
+<assembly
+       xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+       <id>a2-source-tp</id>
+       <baseDirectory></baseDirectory>
+       <formats>
+               <format>dir</format>
+       </formats>
+       <dependencySets>
+               <dependencySet>
+                       <unpack>false</unpack>
+                       <outputFileNameMapping>${artifact.groupId}/${artifact.artifactId}-${artifact.version}.${artifact.extension}</outputFileNameMapping>
+                       <outputDirectory></outputDirectory>
+                       <useTransitiveDependencies>false</useTransitiveDependencies>
+                       <scope>runtime</scope>
+                       <includes>
+                               <include>org.argeo.tp.*:*</include>
+                       </includes>
+               </dependencySet>
+       </dependencySets>
+</assembly>
\ No newline at end of file
diff --git a/maven/assembly-descriptors/assemblies/a2-source.xml b/maven/assembly-descriptors/assemblies/a2-source.xml
new file mode 100644 (file)
index 0000000..9019917
--- /dev/null
@@ -0,0 +1,22 @@
+<assembly
+       xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+       <id>a2-source</id>
+       <baseDirectory></baseDirectory>
+       <formats>
+               <format>dir</format>
+       </formats>
+       <dependencySets>
+               <dependencySet>
+                       <unpack>false</unpack>
+                       <outputFileNameMapping>${artifact.groupId}/${artifact.artifactId}-${artifact.version}.${artifact.extension}</outputFileNameMapping>
+                       <outputDirectory></outputDirectory>
+                       <useTransitiveDependencies>false</useTransitiveDependencies>
+                       <scope>runtime</scope>
+                       <excludes>
+                               <exclude>org.argeo.tp.*:*</exclude>
+                       </excludes>
+               </dependencySet>
+       </dependencySets>
+</assembly>
\ No newline at end of file
diff --git a/maven/assembly-descriptors/bnd.bnd b/maven/assembly-descriptors/bnd.bnd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/maven/assembly-descriptors/pom.xml b/maven/assembly-descriptors/pom.xml
new file mode 100644 (file)
index 0000000..9a863b7
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>maven</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>assembly-descriptors</artifactId>
+       <name>Assembly Descriptors</name>
+</project>
\ No newline at end of file
diff --git a/maven/pom.xml b/maven/pom.xml
new file mode 100644 (file)
index 0000000..ed4a7e7
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>maven</artifactId>
+       <name>Maven Helpers and Templates</name>
+       <packaging>pom</packaging>
+       <modules>
+               <module>assembly-descriptors</module>
+       </modules>
+</project>
\ No newline at end of file
diff --git a/org.argeo.cms.e4.rap/.classpath b/org.argeo.cms.e4.rap/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms.e4.rap/.gitignore b/org.argeo.cms.e4.rap/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.cms.e4.rap/.project b/org.argeo.cms.e4.rap/.project
new file mode 100644 (file)
index 0000000..40c9e01
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.e4.rap</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>
+               <buildCommand>
+                       <name>org.eclipse.pde.ds.core.builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.cms.e4.rap/META-INF/.gitignore b/org.argeo.cms.e4.rap/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.cms.e4.rap/OSGI-INF/cms-admin-rap.xml b/org.argeo.cms.e4.rap/OSGI-INF/cms-admin-rap.xml
new file mode 100644 (file)
index 0000000..26b454a
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" configuration-policy="optional" name="CMS Admin RAP">
+   <implementation class="org.argeo.cms.e4.rap.CmsE4AdminApp"/>
+   <service>
+      <provide interface="org.eclipse.rap.rwt.application.ApplicationConfiguration"/>
+      <property name="contextName" type="String" value="cmsXXX"/>
+   </service>
+</scr:component>
diff --git a/org.argeo.cms.e4.rap/OSGI-INF/cms-demo-rap.xml b/org.argeo.cms.e4.rap/OSGI-INF/cms-demo-rap.xml
new file mode 100644 (file)
index 0000000..e23b11b
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" configuration-policy="optional" name="CMS Demo RAP">
+   <implementation class="org.argeo.cms.e4.rap.CmsE4DemoApp"/>
+   <service>
+      <provide interface="org.eclipse.rap.rwt.application.ApplicationConfiguration"/>
+      <property name="contextName" type="String" value="demoXXX"/>
+   </service>
+</scr:component>
diff --git a/org.argeo.cms.e4.rap/bnd.bnd b/org.argeo.cms.e4.rap/bnd.bnd
new file mode 100644 (file)
index 0000000..32ff6d1
--- /dev/null
@@ -0,0 +1,14 @@
+Bundle-ActivationPolicy: lazy
+Service-Component: OSGI-INF/cms-admin-rap.xml,\
+OSGI-INF/cms-demo-rap.xml
+
+Bundle-Activator: org.argeo.cms.script.ScriptAppActivator
+
+Import-Package: org.argeo.node,\
+org.eclipse.swt,\
+org.eclipse.swt.graphics,\
+org.eclipse.e4.ui.workbench,\
+org.eclipse.rap.rwt.client,\
+org.argeo.cms.script,\
+org.eclipse.nebula.widgets.richtext;resolution:=optional,\
+*
diff --git a/org.argeo.cms.e4.rap/build.properties b/org.argeo.cms.e4.rap/build.properties
new file mode 100644 (file)
index 0000000..c58ea21
--- /dev/null
@@ -0,0 +1,5 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .,\
+               OSGI-INF/
diff --git a/org.argeo.cms.e4.rap/cms/app.js b/org.argeo.cms.e4.rap/cms/app.js
new file mode 100644 (file)
index 0000000..d4c9045
--- /dev/null
@@ -0,0 +1,8 @@
+// Standard CMS App
+APP.webPath = 'cms'
+
+// Common
+APP.pageTitle = 'Argeo CMS';
+
+APP.ui['devops'] = new org.argeo.cms.script.AppUi(APP, new org.argeo.cms.e4.rap.CmsE4EntryPointFactory('org.argeo.cms.e4/e4xmi/cms-devops.e4xmi'));
+APP.ui['devops'].pageTitle = 'Argeo CMS DevOps';
diff --git a/org.argeo.cms.e4.rap/e4xmi/cms-demo-rap.e4xmi b/org.argeo.cms.e4.rap/e4xmi/cms-demo-rap.e4xmi
new file mode 100644 (file)
index 0000000..36064f0
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="ASCII"?>
+<application:Application xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:application="http://www.eclipse.org/ui/2010/UIModel/application" xmlns:basic="http://www.eclipse.org/ui/2010/UIModel/application/ui/basic" xmlns:menu="http://www.eclipse.org/ui/2010/UIModel/application/ui/menu" xmi:id="_PjHLwMb4EeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.application">
+  <children xsi:type="basic:TrimmedWindow" xmi:id="_QnyU0Mb4EeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.trimmedwindow.0">
+    <children xsi:type="basic:PartStack" xmi:id="_V9EXcMb4EeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.partstack.0">
+      <children xsi:type="basic:Part" xmi:id="_RVKlIMctEeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.part.docbook" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.parts.CmsDocBookEditor" label="DocBook">
+        <toolbar xmi:id="_TANxsMctEeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.toolbar.1">
+          <children xsi:type="menu:HandledToolItem" xmi:id="_alIUoMctEeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.handledtoolitem.save" label="Save" command="_vsxg8McmEeiIG7Bq51Btuw"/>
+        </toolbar>
+      </children>
+      <children xsi:type="basic:Part" xmi:id="_fPCGgMcCEeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.part.texteditor" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.parts.CmsTextEditor" label="Text Editor">
+        <toolbar xmi:id="_jlPucMcmEeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.toolbar.0">
+          <children xsi:type="menu:HandledToolItem" xmi:id="_r3TEMMcmEeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.handledtoolitem.0" command="_vsxg8McmEeiIG7Bq51Btuw"/>
+        </toolbar>
+      </children>
+      <children xsi:type="basic:Part" xmi:id="_cIlegMb4EeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.part.htmleditor" contributionURI="bundleclass://org.argeo.cms.e4.rap/org.argeo.cms.e4.rap.parts.HtmlEditor" label="HTML Editor"/>
+    </children>
+  </children>
+  <handlers xmi:id="_zqabMMcmEeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.handler.0" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.handlers.SavePart" command="_vsxg8McmEeiIG7Bq51Btuw"/>
+  <commands xmi:id="_vsxg8McmEeiIG7Bq51Btuw" elementId="org.argeo.cms.e4.command.save" commandName="Save"/>
+  <addons xmi:id="_PjHLwcb4EeiIG7Bq51Btuw" elementId="org.eclipse.e4.core.commands.service" contributionURI="bundleclass://org.eclipse.e4.core.commands/org.eclipse.e4.core.commands.CommandServiceAddon"/>
+  <addons xmi:id="_PjHLwsb4EeiIG7Bq51Btuw" elementId="org.eclipse.e4.ui.contexts.service" contributionURI="bundleclass://org.eclipse.e4.ui.services/org.eclipse.e4.ui.services.ContextServiceAddon"/>
+  <addons xmi:id="_PjHLw8b4EeiIG7Bq51Btuw" elementId="org.eclipse.e4.ui.bindings.service" contributionURI="bundleclass://org.eclipse.e4.ui.bindings/org.eclipse.e4.ui.bindings.BindingServiceAddon"/>
+  <addons xmi:id="_PjHLxMb4EeiIG7Bq51Btuw" elementId="org.eclipse.e4.ui.workbench.commands.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.CommandProcessingAddon"/>
+  <addons xmi:id="_PjHLxcb4EeiIG7Bq51Btuw" elementId="org.eclipse.e4.ui.workbench.contexts.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.ContextProcessingAddon"/>
+  <addons xmi:id="_PjHLxsb4EeiIG7Bq51Btuw" elementId="org.eclipse.e4.ui.workbench.bindings.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench.swt/org.eclipse.e4.ui.workbench.swt.util.BindingProcessingAddon"/>
+  <addons xmi:id="_PjHLx8b4EeiIG7Bq51Btuw" elementId="org.eclipse.e4.ui.workbench.handler.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.HandlerProcessingAddon"/>
+</application:Application>
diff --git a/org.argeo.cms.e4.rap/pom.xml b/org.argeo.cms.e4.rap/pom.xml
new file mode 100644 (file)
index 0000000..3ac5404
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.cms.e4.rap</artifactId>
+       <name>CMS E4 RAP</name>
+       <packaging>jar</packaging>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.e4</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <!-- Specific -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <scope>provided</scope>
+               </dependency>
+
+               <!-- UI -->
+               <dependency>
+                       <groupId>org.argeo.tp</groupId>
+                       <artifactId>argeo-tp-rap-e4</artifactId>
+                       <version>${version.argeo-tp}</version>
+                       <type>pom</type>
+                       <scope>provided</scope>
+               </dependency>
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/AbstractRapE4App.java b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/AbstractRapE4App.java
new file mode 100644 (file)
index 0000000..66c796b
--- /dev/null
@@ -0,0 +1,57 @@
+package org.argeo.cms.e4.rap;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.argeo.cms.ui.dialogs.CmsFeedback;
+import org.eclipse.rap.e4.E4ApplicationConfig;
+import org.eclipse.rap.rwt.application.Application;
+import org.eclipse.rap.rwt.application.Application.OperationMode;
+import org.eclipse.rap.rwt.application.ApplicationConfiguration;
+import org.eclipse.rap.rwt.application.ExceptionHandler;
+import org.eclipse.rap.rwt.client.WebClient;
+
+public abstract class AbstractRapE4App implements ApplicationConfiguration {
+       private String pageTitle;
+       private String e4Xmi;
+       private String path;
+       private String lifeCycleUri = "bundleclass://org.argeo.cms.e4.rap/org.argeo.cms.e4.rap.CmsLoginLifecycle";
+
+       public void configure(Application application) {
+               application.setExceptionHandler(new ExceptionHandler() {
+
+                       @Override
+                       public void handleException(Throwable throwable) {
+                               CmsFeedback.show("Unexpected RWT exception", throwable);
+                       }
+               });
+
+               Map<String, String> properties = new HashMap<String, String>();
+               properties.put(WebClient.PAGE_TITLE, pageTitle);
+               E4ApplicationConfig config = new E4ApplicationConfig(e4Xmi, lifeCycleUri, null, null, false, true, true);
+               addEntryPoint(application, config, properties);
+       }
+
+       protected void addEntryPoint(Application application, E4ApplicationConfig config, Map<String, String> properties) {
+               CmsE4EntryPointFactory entryPointFactory = new CmsE4EntryPointFactory(config);
+               application.addEntryPoint(path, entryPointFactory, properties);
+               application.setOperationMode(OperationMode.SWT_COMPATIBILITY);
+       }
+
+       public void setPageTitle(String pageTitle) {
+               this.pageTitle = pageTitle;
+       }
+
+       public void setE4Xmi(String e4Xmi) {
+               this.e4Xmi = e4Xmi;
+       }
+
+       public void setPath(String path) {
+               this.path = path;
+       }
+
+       public void setLifeCycleUri(String lifeCycleUri) {
+               this.lifeCycleUri = lifeCycleUri;
+       }
+
+}
diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4AdminApp.java b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4AdminApp.java
new file mode 100644 (file)
index 0000000..4921680
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.cms.e4.rap;
+
+public class CmsE4AdminApp extends AbstractRapE4App {
+       public CmsE4AdminApp() {
+               setPageTitle("CMS Admin");
+               setE4Xmi("org.argeo.cms.e4/e4xmi/cms-devops.e4xmi");
+               setPath("/devops");
+       }
+
+}
diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4DemoApp.java b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4DemoApp.java
new file mode 100644 (file)
index 0000000..2987e4d
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.cms.e4.rap;
+
+public class CmsE4DemoApp extends AbstractRapE4App {
+       public CmsE4DemoApp() {
+               setPageTitle("CMS Demo");
+               setE4Xmi("org.argeo.cms.e4.rap/e4xmi/cms-demo-rap.e4xmi");
+               setPath("/cms-e4");
+       }
+
+}
diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4EntryPointFactory.java b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4EntryPointFactory.java
new file mode 100644 (file)
index 0000000..a5a3234
--- /dev/null
@@ -0,0 +1,76 @@
+package org.argeo.cms.e4.rap;
+
+import java.security.PrivilegedAction;
+
+import javax.security.auth.Subject;
+
+import org.eclipse.rap.e4.E4ApplicationConfig;
+import org.eclipse.rap.e4.E4EntryPointFactory;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.application.EntryPoint;
+import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
+
+public class CmsE4EntryPointFactory extends E4EntryPointFactory {
+       public final static String DEFAULT_LIFECYCLE_URI = "bundleclass://org.argeo.cms.e4.rap/org.argeo.cms.e4.rap.CmsLoginLifecycle";
+
+       public CmsE4EntryPointFactory(E4ApplicationConfig config) {
+               super(config);
+       }
+
+       public CmsE4EntryPointFactory(String e4Xmi, String lifeCycleUri) {
+               super(defaultConfig(e4Xmi, lifeCycleUri));
+       }
+
+       public CmsE4EntryPointFactory(String e4Xmi) {
+               this(e4Xmi, DEFAULT_LIFECYCLE_URI);
+       }
+
+       public static E4ApplicationConfig defaultConfig(String e4Xmi, String lifeCycleUri) {
+               E4ApplicationConfig config = new E4ApplicationConfig(e4Xmi, lifeCycleUri, null, null, false, true, true);
+               return config;
+       }
+
+       @Override
+       public EntryPoint create() {
+               EntryPoint ep = createEntryPoint();
+               EntryPoint authEp = new EntryPoint() {
+
+                       @Override
+                       public int createUI() {
+                               Subject subject = new Subject();
+                               return Subject.doAs(subject, new PrivilegedAction<Integer>() {
+
+                                       @Override
+                                       public Integer run() {
+                                               // SPNEGO
+                                               // HttpServletRequest request = RWT.getRequest();
+                                               // String authorization = request.getHeader(HEADER_AUTHORIZATION);
+                                               // if (authorization == null || !authorization.startsWith("Negotiate")) {
+                                               // HttpServletResponse response = RWT.getResponse();
+                                               // response.setStatus(401);
+                                               // response.setHeader(HEADER_WWW_AUTHENTICATE, "Negotiate");
+                                               // response.setDateHeader("Date", System.currentTimeMillis());
+                                               // response.setDateHeader("Expires", System.currentTimeMillis() + (24 * 60 * 60
+                                               // * 1000));
+                                               // response.setHeader("Accept-Ranges", "bytes");
+                                               // response.setHeader("Connection", "Keep-Alive");
+                                               // response.setHeader("Keep-Alive", "timeout=5, max=97");
+                                               // // response.setContentType("text/html; charset=UTF-8");
+                                               // }
+
+                                               JavaScriptExecutor jsExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
+                                               Integer exitCode = ep.createUI();
+                                               jsExecutor.execute("location.reload()");
+                                               return exitCode;
+                                       }
+
+                               });
+                       }
+               };
+               return authEp;
+       }
+
+       protected EntryPoint createEntryPoint() {
+               return super.create();
+       }
+}
diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsLoginLifecycle.java b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsLoginLifecycle.java
new file mode 100644 (file)
index 0000000..a062bea
--- /dev/null
@@ -0,0 +1,175 @@
+package org.argeo.cms.e4.rap;
+
+import java.security.AccessController;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.CmsImageManager;
+import org.argeo.cms.ui.CmsView;
+import org.argeo.cms.ui.UxContext;
+import org.argeo.cms.ui.dialogs.CmsFeedback;
+import org.argeo.cms.util.SimpleImageManager;
+import org.argeo.cms.util.SimpleUxContext;
+import org.argeo.cms.widgets.auth.CmsLoginShell;
+import org.argeo.eclipse.ui.specific.UiContext;
+import org.argeo.node.NodeConstants;
+import org.eclipse.e4.core.services.events.IEventBroker;
+import org.eclipse.e4.ui.workbench.UIEvents;
+import org.eclipse.e4.ui.workbench.lifecycle.PostContextCreate;
+import org.eclipse.e4.ui.workbench.lifecycle.PreSave;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.client.service.BrowserNavigation;
+import org.eclipse.rap.rwt.client.service.BrowserNavigationEvent;
+import org.eclipse.rap.rwt.client.service.BrowserNavigationListener;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventHandler;
+
+@SuppressWarnings("restriction")
+public class CmsLoginLifecycle implements CmsView {
+       private final static Log log = LogFactory.getLog(CmsLoginLifecycle.class);
+
+       private UxContext uxContext;
+       private CmsImageManager imageManager;
+
+       private LoginContext loginContext;
+       private BrowserNavigation browserNavigation;
+
+       private String state = null;
+
+       @PostContextCreate
+       boolean login(final IEventBroker eventBroker) {
+               browserNavigation = RWT.getClient().getService(BrowserNavigation.class);
+               if (browserNavigation != null)
+                       browserNavigation.addBrowserNavigationListener(new BrowserNavigationListener() {
+                               private static final long serialVersionUID = -3668136623771902865L;
+
+                               @Override
+                               public void navigated(BrowserNavigationEvent event) {
+                                       state = event.getState();
+                                       if (uxContext != null)// is logged in
+                                               stateChanged();
+                               }
+                       });
+
+               Subject subject = Subject.getSubject(AccessController.getContext());
+               Display display = Display.getCurrent();
+               UiContext.setData(CmsView.KEY, this);
+               CmsLoginShell loginShell = new CmsLoginShell(this);
+               loginShell.setSubject(subject);
+               try {
+                       // try pre-auth
+                       loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, subject, loginShell);
+                       loginContext.login();
+               } catch (LoginException e) {
+                       loginShell.createUi();
+                       loginShell.open();
+
+                       while (!loginShell.getShell().isDisposed()) {
+                               if (!display.readAndDispatch())
+                                       display.sleep();
+                       }
+               }
+               if (CurrentUser.getUsername(getSubject()) == null)
+                       return false;
+               uxContext = new SimpleUxContext();
+               imageManager = new SimpleImageManager();
+
+               eventBroker.subscribe(UIEvents.UILifeCycle.APP_STARTUP_COMPLETE, new EventHandler() {
+                       @Override
+                       public void handleEvent(Event event) {
+                               startupComplete();
+                               eventBroker.unsubscribe(this);
+                       }
+               });
+
+               // lcs.changeApplicationLocale(Locale.FRENCH);
+               return true;
+       }
+
+       @PreSave
+       void destroy() {
+               // logout();
+       }
+
+       @Override
+       public UxContext getUxContext() {
+               return uxContext;
+       }
+
+       @Override
+       public void navigateTo(String state) {
+               browserNavigation.pushState(state, state);
+       }
+
+       @Override
+       public void authChange(LoginContext loginContext) {
+               if (loginContext == null)
+                       throw new CmsException("Login context cannot be null");
+               // logout previous login context
+               // if (this.loginContext != null)
+               // try {
+               // this.loginContext.logout();
+               // } catch (LoginException e1) {
+               // System.err.println("Could not log out: " + e1);
+               // }
+               this.loginContext = loginContext;
+       }
+
+       @Override
+       public void logout() {
+               if (loginContext == null)
+                       throw new CmsException("Login context should not be null");
+               try {
+                       CurrentUser.logoutCmsSession(loginContext.getSubject());
+                       loginContext.logout();
+               } catch (LoginException e) {
+                       throw new CmsException("Cannot log out", e);
+               }
+       }
+
+       @Override
+       public void exception(Throwable e) {
+               String msg = "Unexpected exception in Eclipse 4 RAP";
+               log.error(msg, e);
+               CmsFeedback.show(msg, e);
+       }
+
+       @Override
+       public CmsImageManager getImageManager() {
+               return imageManager;
+       }
+
+       protected Subject getSubject() {
+               return loginContext.getSubject();
+       }
+
+       @Override
+       public boolean isAnonymous() {
+               return CurrentUser.isAnonymous(getSubject());
+       }
+
+       // CALLBACKS
+       protected void startupComplete() {
+       }
+
+       protected void stateChanged() {
+
+       }
+
+       // GETTERS
+       protected BrowserNavigation getBrowserNavigation() {
+               return browserNavigation;
+       }
+
+       protected String getState() {
+               return state;
+       }
+
+}
diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/CkEditor-removeButtons.js b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/CkEditor-removeButtons.js
new file mode 100644 (file)
index 0000000..20e82e3
--- /dev/null
@@ -0,0 +1 @@
+'Source,Save,Templates,Strike,Subscript,Superscript,CopyFormatting,Outdent,Indent,CreateDiv,JustifyLeft,JustifyCenter,JustifyRight,JustifyBlock,Language,Anchor,Flash,HorizontalRule,Smiley,SpecialChar,PageBreak,Iframe,Format,Font,FontSize,BGColor,TextColor,ShowBlocks,About,Preview,Print,Redo,Replace,Find,Undo,SelectAll,Scayt,Form,Checkbox,Radio,TextField,Textarea,Select,Button,ImageButton,HiddenField,NewPage,PasteFromWord,Blockquote,BidiLtr,BidiRtl'
\ No newline at end of file
diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/CkEditor-toolbar.js b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/CkEditor-toolbar.js
new file mode 100644 (file)
index 0000000..3058655
--- /dev/null
@@ -0,0 +1,19 @@
+CKEDITOR.editorConfig = function( config ) {
+       config.toolbarGroups = [
+               { name: 'document', groups: [ 'mode', 'document', 'doctools' ] },
+               { name: 'styles', groups: [ 'styles' ] },
+               { name: 'editing', groups: [ 'find', 'selection', 'spellchecker', 'editing' ] },
+               { name: 'forms', groups: [ 'forms' ] },
+               { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
+               { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align', 'bidi', 'paragraph' ] },
+               { name: 'insert', groups: [ 'insert' ] },
+               { name: 'links', groups: [ 'links' ] },
+               { name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
+               { name: 'colors', groups: [ 'colors' ] },
+               { name: 'tools', groups: [ 'tools' ] },
+               { name: 'others', groups: [ 'others' ] },
+               { name: 'about', groups: [ 'about' ] }
+       ];
+
+       config.removeButtons = 'Source,Save,Templates,Strike,Subscript,Superscript,CopyFormatting,Outdent,Indent,CreateDiv,JustifyLeft,JustifyCenter,JustifyRight,JustifyBlock,BidiLtr,BidiRtl,Language,Anchor,Flash,HorizontalRule,Smiley,SpecialChar,PageBreak,Iframe,Format,Font,FontSize,BGColor,TextColor,Maximize,ShowBlocks,About,Preview,Print,Redo,Replace,Find,Undo,SelectAll,Scayt,Form,Checkbox,Radio,TextField,Textarea,Select,Button,ImageButton,HiddenField,NewPage,PasteFromWord,Blockquote';
+};
\ No newline at end of file
diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/CkEditor-toolbarGroups.json b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/CkEditor-toolbarGroups.json
new file mode 100644 (file)
index 0000000..a886c27
--- /dev/null
@@ -0,0 +1,92 @@
+[
+       {
+               "name": "document",
+               "groups": [
+                       "mode",
+                       "document",
+                       "doctools"
+               ]
+       },
+       {
+               "name": "styles",
+               "groups": [
+                       "styles"
+               ]
+       },
+       {
+               "name": "editing",
+               "groups": [
+                       "find",
+                       "selection",
+                       "spellchecker",
+                       "editing"
+               ]
+       },
+       {
+               "name": "forms",
+               "groups": [
+                       "forms"
+               ]
+       },
+       {
+               "name": "basicstyles",
+               "groups": [
+                       "basicstyles",
+                       "cleanup"
+               ]
+       },
+       {
+               "name": "paragraph",
+               "groups": [
+                       "list",
+                       "indent",
+                       "blocks",
+                       "align",
+                       "bidi",
+                       "paragraph"
+               ]
+       },
+       {
+               "name": "insert",
+               "groups": [
+                       "insert"
+               ]
+       },
+       {
+               "name": "links",
+               "groups": [
+                       "links"
+               ]
+       },
+       {
+               "name": "clipboard",
+               "groups": [
+                       "clipboard",
+                       "undo"
+               ]
+       },
+       {
+               "name": "colors",
+               "groups": [
+                       "colors"
+               ]
+       },
+       {
+               "name": "tools",
+               "groups": [
+                       "tools"
+               ]
+       },
+       {
+               "name": "others",
+               "groups": [
+                       "others"
+               ]
+       },
+       {
+               "name": "about",
+               "groups": [
+                       "about"
+               ]
+       }
+]
\ No newline at end of file
diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/HtmlEditor.java b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/HtmlEditor.java
new file mode 100644 (file)
index 0000000..68963f9
--- /dev/null
@@ -0,0 +1,127 @@
+package org.argeo.cms.e4.rap.parts;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import javax.annotation.PostConstruct;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.nebula.widgets.richtext.RichTextEditor;
+import org.eclipse.nebula.widgets.richtext.RichTextEditorConfiguration;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.browser.Browser;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+public class HtmlEditor {
+
+       @PostConstruct
+       public void createUI(Composite parent) {
+               String toolbarGroups;
+               String removeButtons;
+               try {
+                       toolbarGroups = IOUtils.toString(HtmlEditor.class.getResourceAsStream("CkEditor-toolbarGroups.json"),
+                                       StandardCharsets.UTF_8);
+                       removeButtons = IOUtils.toString(HtmlEditor.class.getResourceAsStream("CkEditor-removeButtons.js"),
+                                       StandardCharsets.UTF_8);
+               } catch (IOException e) {
+                       throw new CmsException("Cannot configure toolbar", e);
+               }
+//             System.out.println(toolbarGroups);
+//             System.out.println(removeButtons);
+               RichTextEditorConfiguration richTextEditorConfig = new RichTextEditorConfiguration();
+               richTextEditorConfig.setOption(RichTextEditorConfiguration.TOOLBAR_GROUPS, toolbarGroups);
+               richTextEditorConfig.setOption(RichTextEditorConfiguration.REMOVE_BUTTONS, removeButtons);
+//             richTextEditorConfig.setRemoveStyles(false);
+//             richTextEditorConfig.setRemovePasteFromWord(true);
+//             richTextEditorConfig.setRemovePasteText(false);
+
+//             richTextEditorConfig.setToolbarCollapsible(true);
+//             richTextEditorConfig.setToolbarInitialExpanded(false);
+               
+               final Display display = parent.getDisplay();
+               Composite composite = new Composite(parent, SWT.NONE);
+//             composite.setLayoutData(new Fill);
+               composite.setLayout(new GridLayout());
+               final RichTextEditor richTextEditor = new RichTextEditor(composite, richTextEditorConfig, SWT.BORDER);
+               richTextEditor.setText("<a href='http://googl.com'>Google</a>");
+               GridData layoutData = new GridData(SWT.FILL, SWT.FILL, true, true);
+               richTextEditor.setLayoutData(layoutData);
+               richTextEditor.setBackground(new Color(display, 247, 247, 247));
+               Composite toolbar = new Composite(composite, SWT.NONE);
+               toolbar.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               toolbar.setLayout(new GridLayout(3, false));
+               Button showContent = new Button(toolbar, SWT.PUSH);
+               showContent.setText("Show Content");
+               showContent.addSelectionListener(new SelectionAdapter() {
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               showContent(parent, richTextEditor, false);
+                       }
+               });
+               Button showSource = new Button(toolbar, SWT.PUSH);
+               showSource.setText("Show Source");
+               showSource.addSelectionListener(new SelectionAdapter() {
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               showContent(parent, richTextEditor, true);
+                       }
+               });
+               Button clearBtn = new Button(toolbar, SWT.NONE);
+               clearBtn.setText("Clear");
+               clearBtn.addSelectionListener(new SelectionAdapter() {
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               richTextEditor.setText("");
+                       }
+               });
+
+       }
+
+       private static void showContent(Composite parent, RichTextEditor editor, boolean source) {
+               int style = SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL;
+               final Shell content = new Shell(parent.getShell(), style);
+               content.setLayout(new GridLayout(1, true));
+               String text = editor.getText();
+               if (source) {
+                       content.setText("Rich Text Source");
+                       Text viewer = new Text(content, SWT.MULTI | SWT.WRAP);
+                       viewer.setLayoutData(new GridData(400, 400));
+                       viewer.setText(text);
+                       viewer.setEditable(false);
+               } else {
+                       content.setText("Rich Text");
+                       Browser viewer = new Browser(content, SWT.NONE);
+                       viewer.setLayoutData(new GridData(400, 400));
+                       viewer.setText(text);
+                       viewer.setEnabled(false);
+               }
+               Button ok = new Button(content, SWT.PUSH);
+               ok.setLayoutData(new GridData(SWT.RIGHT, SWT.BOTTOM, false, false));
+               ok.setText("OK");
+               ok.addSelectionListener(new SelectionAdapter() {
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               content.dispose();
+                       }
+               });
+               content.setDefaultButton(ok);
+               content.pack();
+               Display display = parent.getDisplay();
+               int left = (display.getClientArea().width / 2) - (content.getBounds().width / 2);
+               content.setLocation(left, 40);
+               content.open();
+               ok.setFocus();
+       }
+
+}
diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/test.json b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/parts/test.json
new file mode 100644 (file)
index 0000000..eed3f0e
--- /dev/null
@@ -0,0 +1,28 @@
+{
+       "firstName": "John",
+       "lastName": "Smith",
+       "isAlive": true,
+       "age": 27,
+       "address": {
+               "streetAddress": "21 2nd Street",
+               "city": "New York",
+               "state": "NY",
+               "postalCode": "10021-3100"
+       },
+       "phoneNumbers": [
+               {
+                       "type": "home",
+                       "number": "212 555-1234"
+               },
+               {
+                       "type": "office",
+                       "number": "646 555-4567"
+               },
+               {
+                       "type": "mobile",
+                       "number": "123 456-7890"
+               }
+       ],
+       "children": [],
+       "spouse": null
+}
diff --git a/org.argeo.cms.e4/.classpath b/org.argeo.cms.e4/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms.e4/.gitignore b/org.argeo.cms.e4/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.cms.e4/.project b/org.argeo.cms.e4/.project
new file mode 100644 (file)
index 0000000..0c04069
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.e4</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>
+               <buildCommand>
+                       <name>org.eclipse.pde.ds.core.builder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.cms.e4/.settings/org.eclipse.jdt.core.prefs b/org.argeo.cms.e4/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..0c68a61
--- /dev/null
@@ -0,0 +1,7 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/org.argeo.cms.e4/META-INF/.gitignore b/org.argeo.cms.e4/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.cms.e4/OSGI-INF/defaultCallbackHandler.xml b/org.argeo.cms.e4/OSGI-INF/defaultCallbackHandler.xml
new file mode 100644 (file)
index 0000000..8653f09
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="Default CallbackHandler">
+   <implementation class="org.argeo.cms.widgets.auth.DynamicCallbackHandler"/>
+   <service>
+      <provide interface="javax.security.auth.callback.CallbackHandler"/>
+   </service>
+</scr:component>
diff --git a/org.argeo.cms.e4/OSGI-INF/homeRepository.xml b/org.argeo.cms.e4/OSGI-INF/homeRepository.xml
new file mode 100644 (file)
index 0000000..c03e62e
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" name="Home Repository">
+   <implementation class="org.argeo.cms.e4.contexts.OsgiFilterContextFunction"/>
+   <property name="service.context.key" type="String" value="(cn=home)"/>
+   <service>
+      <provide interface="org.eclipse.e4.core.contexts.IContextFunction"/>
+   </service>
+</scr:component>
diff --git a/org.argeo.cms.e4/OSGI-INF/userAdminWrapper.xml b/org.argeo.cms.e4/OSGI-INF/userAdminWrapper.xml
new file mode 100644 (file)
index 0000000..4b9d021
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" immediate="true" name="User Admin Wrapper">
+   <implementation class="org.argeo.cms.e4.users.UserAdminWrapper"/>
+   <reference bind="setUserTransaction" cardinality="1..1" interface="javax.transaction.UserTransaction" name="UserTransaction" policy="static"/>
+   <reference bind="setUserAdmin" cardinality="1..1" interface="org.osgi.service.useradmin.UserAdmin" name="UserAdmin" policy="static"/>
+   <service>
+      <provide interface="org.argeo.cms.e4.users.UserAdminWrapper"/>
+   </service>
+</scr:component>
diff --git a/org.argeo.cms.e4/bnd.bnd b/org.argeo.cms.e4/bnd.bnd
new file mode 100644 (file)
index 0000000..7839290
--- /dev/null
@@ -0,0 +1,16 @@
+Service-Component: OSGI-INF/homeRepository.xml,\
+OSGI-INF/userAdminWrapper.xml,\
+OSGI-INF/defaultCallbackHandler.xml
+Bundle-ActivationPolicy: lazy
+
+Import-Package: org.eclipse.swt,\
+org.eclipse.swt.widgets;version="0.0.0",\
+org.eclipse.e4.ui.model.application.ui,\
+org.eclipse.e4.ui.model.application,\
+javax.jcr.nodetype,\
+org.argeo.jcr.docbook,\
+org.eclipse.core.commands.common,\
+org.eclipse.jface.window,\
+org.argeo.cms.widgets.auth,\
+org.argeo.cms.i18n,\
+*
diff --git a/org.argeo.cms.e4/build.properties b/org.argeo.cms.e4/build.properties
new file mode 100644 (file)
index 0000000..e46a7ba
--- /dev/null
@@ -0,0 +1,9 @@
+output.. = bin/
+bin.includes = META-INF/,\
+               OSGI-INF/,\
+               .,\
+               OSGI-INF/homeRepository.xml,\
+               OSGI-INF/userAdminWrapper.xml,\
+               OSGI-INF/defaultCallbackHandler.xml,\
+               e4xmi/cms-demo.e4xmi
+source.. = src/
diff --git a/org.argeo.cms.e4/e4xmi/cms-devops.e4xmi b/org.argeo.cms.e4/e4xmi/cms-devops.e4xmi
new file mode 100644 (file)
index 0000000..beff27d
--- /dev/null
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="ASCII"?>
+<application:Application xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:advanced="http://www.eclipse.org/ui/2010/UIModel/application/ui/advanced" xmlns:application="http://www.eclipse.org/ui/2010/UIModel/application" xmlns:basic="http://www.eclipse.org/ui/2010/UIModel/application/ui/basic" xmlns:menu="http://www.eclipse.org/ui/2010/UIModel/application/ui/menu" xmi:id="_XqkCQKknEeObFrG_clJBYA" elementId="">
+  <children xsi:type="basic:TrimmedWindow" xmi:id="_Zdy6cKknEeObFrG_clJBYA" elementId="org.argeo.cms.e4.apps.admin.trimmedwindow.0" label="" x="10" y="10" width="500" height="500">
+    <persistedState key="styleOverride" value="8"/>
+    <tags>shellMaximized</tags>
+    <tags>auth.cn=admin,ou=roles,ou=node</tags>
+    <children xsi:type="advanced:PerspectiveStack" xmi:id="_jXVqsCk4Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.perspectivestack.0" selectedElement="_jvjWYCk4Eein5vuhpK-Dew">
+      <children xsi:type="advanced:Perspective" xmi:id="_jvjWYCk4Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.perspective.data" label="Data" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/nodes.gif">
+        <children xsi:type="basic:PartSashContainer" xmi:id="_h3tvMCkxEein5vuhpK-Dew" elementId="org.argeo.cms.e4.partsashcontainer.0" selectedElement="_0B9SECkxEein5vuhpK-Dew" horizontal="true">
+          <children xsi:type="basic:PartStack" xmi:id="_0B9SECkxEein5vuhpK-Dew" elementId="org.argeo.cms.e4.partstack.0" containerData="4000" selectedElement="_WAjPkCkTEein5vuhpK-Dew">
+            <children xsi:type="basic:Part" xmi:id="_WAjPkCkTEein5vuhpK-Dew" elementId="org.argeo.cms.e4.jcrbrowser" containerData="" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.jcr.JcrBrowserView" label="JCR" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/browser.gif">
+              <menus xsi:type="menu:PopupMenu" xmi:id="_eXiUECqREeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.popupmenu.nodeViewer">
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_GVeO8CqhEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.refresh" label="Refresh" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/refresh.png" command="_TOKHsCqYEeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_fU238CqREeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.addfoldernode" label="Add folder" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/addFolder.gif" command="_RgE5cCqREeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_U4o9cCqhEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.rename" label="Rename" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/rename.gif" command="_ZrcUMCqYEeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_Ncxo0CqhEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.remove" label="Remove" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/remove.gif" command="_ChJ-4CqYEeidr6NYQH6GbQ"/>
+              </menus>
+              <menus xmi:id="_oRg_ACqTEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.menu.0">
+                <tags>ViewMenu</tags>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_yJR8ECqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.refresh" label="Refresh" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/refresh.png" command="_TOKHsCqYEeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_o6HQECqTEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.addfoldernode" label="Add folder" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/addFolder.gif" command="_RgE5cCqREeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_5D7aACqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.rename" label="Rename" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/rename.gif" command="_ZrcUMCqYEeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_7rR2wCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handledmenuitem.delete" label="Delete" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/remove.gif" command="_ChJ-4CqYEeidr6NYQH6GbQ"/>
+                <children xsi:type="menu:HandledMenuItem" xmi:id="_XsHLgFgQEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.handledmenuitem.0" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/addRepo.gif" command="_ZWpasFgQEeiknZQLx-vtnA"/>
+              </menus>
+            </children>
+          </children>
+          <children xsi:type="basic:PartStack" xmi:id="_mHrEUCk4Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.partstack.1" containerData="6000">
+            <tags>dataExplorer</tags>
+          </children>
+        </children>
+      </children>
+      <children xsi:type="advanced:Perspective" xmi:id="_xOVlsDvOEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.perspective.users" label="Users" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/group.png">
+        <tags>auth.cn=admin,ou=roles,ou=node</tags>
+        <children xsi:type="basic:PartSashContainer" xmi:id="_1tQoEDvOEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partsashcontainer.2" horizontal="true">
+          <children xsi:type="basic:PartStack" xmi:id="_vtbKkDvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partstack.4" containerData="4000" selectedElement="_9gukYDvOEeiF1foPJZSZkw">
+            <children xsi:type="basic:Part" xmi:id="_9gukYDvOEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.part.users" containerData="" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.users.UsersView" label="Users" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/person.png">
+              <handlers xmi:id="_0mN68DvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handler.4" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.users.handlers.NewUser" command="_uL5i4DvjEeiF1foPJZSZkw"/>
+              <handlers xmi:id="_ODLdgDvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handler.5" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.users.handlers.DeleteUsers" command="_xkcMADvjEeiF1foPJZSZkw"/>
+              <toolbar xmi:id="_jLWmkDvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.toolbar.1">
+                <children xsi:type="menu:HandledToolItem" xmi:id="_jy_OUDvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.new" label="New" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/add.png" command="_uL5i4DvjEeiF1foPJZSZkw"/>
+                <children xsi:type="menu:HandledToolItem" xmi:id="_9qszMDvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.delete" label="Delete" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/delete.png" command="_xkcMADvjEeiF1foPJZSZkw"/>
+              </toolbar>
+            </children>
+          </children>
+          <children xsi:type="basic:PartStack" xmi:id="__g1a8DvOEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partstack.3" containerData="4000">
+            <tags>usersEditorArea</tags>
+          </children>
+          <children xsi:type="basic:PartStack" xmi:id="_-mFn8DvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partstack.5" containerData="2000">
+            <children xsi:type="basic:Part" xmi:id="_6etk4DvOEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.part.groups" containerData="" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.users.GroupsView" label="Groups" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/group.png">
+              <handlers xmi:id="_cmShoDvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handler.6" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.users.handlers.NewGroup" command="_uL5i4DvjEeiF1foPJZSZkw"/>
+              <handlers xmi:id="_fbYfcDvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handler.7" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.users.handlers.DeleteGroups" command="_xkcMADvjEeiF1foPJZSZkw"/>
+              <toolbar xmi:id="_Us0rADvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.toolbar.2">
+                <children xsi:type="menu:HandledToolItem" xmi:id="_VQTLgDvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.new" label="New" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/add.png" command="_uL5i4DvjEeiF1foPJZSZkw"/>
+                <children xsi:type="menu:HandledToolItem" xmi:id="_XfME8DvkEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.delete" label="Delete" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/delete.png" command="_xkcMADvjEeiF1foPJZSZkw"/>
+              </toolbar>
+            </children>
+          </children>
+        </children>
+      </children>
+      <children xsi:type="advanced:Perspective" xmi:id="_ABK2ADsNEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.perspective.files" label="Files" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/file.gif">
+        <children xsi:type="basic:PartSashContainer" xmi:id="_FPimEDsSEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.partsashcontainer.1" horizontal="true">
+          <children xsi:type="basic:PartStack" xmi:id="_H93NgDsSEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.partstack.2" containerData="4000">
+            <children xsi:type="basic:Part" xmi:id="_Izxh0DsSEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.part.files" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.files.NodeFsBrowserView" label="Files" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/file.gif"/>
+          </children>
+          <children xsi:type="basic:Part" xmi:id="_TMqBMDsSEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.part.0" containerData="6000"/>
+        </children>
+      </children>
+      <children xsi:type="advanced:Perspective" xmi:id="_u5ZakFhJEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.perspective.monitoring" label="Monitoring" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/bundles.gif">
+        <children xsi:type="basic:PartStack" xmi:id="_7i7t8FhJEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.partstack.6">
+          <children xsi:type="basic:Part" xmi:id="_Z-3cMFhbEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.part.osgiConfigurations" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.monitoring.OsgiConfigurationsView" label="OSGi Configurations" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/node.gif"/>
+          <children xsi:type="basic:Part" xmi:id="_8dM90FhJEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.part.cmsSessions" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.monitoring.CmsSessionsView" label="CMS Sessions" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/person-logged-in.png"/>
+          <children xsi:type="basic:Part" xmi:id="_KqRZIFhNEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.part.modules" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.monitoring.ModulesView" label="Modules" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/bundles.gif"/>
+          <children xsi:type="basic:Part" xmi:id="_dXtIoFhNEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.part.bundles" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.monitoring.BundlesView" label="Bundles" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/bundles.gif"/>
+        </children>
+      </children>
+    </children>
+    <handlers xmi:id="_Vwax0DvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handler.8" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.handlers.OpenPerspective" command="_AF1UsDvrEeiF1foPJZSZkw"/>
+    <trimBars xmi:id="_euVxMCk2Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.trimbar.0" side="Left">
+      <children xsi:type="menu:ToolBar" xmi:id="_fotHsCk2Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.toolbar.0">
+        <children xsi:type="menu:HandledToolItem" xmi:id="_jfUM4Ck2Eein5vuhpK-Dew" elementId="org.argeo.cms.e4.handledtoolitem.test" label="Data" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/nodes.gif" command="_AF1UsDvrEeiF1foPJZSZkw">
+          <parameters xmi:id="_KDlXQDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.parameter.0" name="perspectiveId" value="org.argeo.cms.e4.perspective.data"/>
+        </children>
+        <children xsi:type="menu:HandledToolItem" xmi:id="_b0OHUDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.files" label="Files" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/file.gif" command="_AF1UsDvrEeiF1foPJZSZkw">
+          <parameters xmi:id="_fXvRYDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.parameter.1" name="perspectiveId" value="org.argeo.cms.e4.perspective.files"/>
+        </children>
+        <children xsi:type="menu:HandledToolItem" xmi:id="_jCSQgDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.handledtoolitem.users" label="Users" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/group.png" command="_AF1UsDvrEeiF1foPJZSZkw">
+          <tags>auth.cn=admin,ou=roles,ou=node</tags>
+          <parameters xmi:id="_lu_uYDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.parameter.2" name="perspectiveId" value="org.argeo.cms.e4.perspective.users"/>
+        </children>
+        <children xsi:type="menu:HandledToolItem" xmi:id="_dhv80FhKEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.handledtoolitem.monitoring" label="Monitoring" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/bundles.gif" command="_AF1UsDvrEeiF1foPJZSZkw">
+          <parameters xmi:id="_kjN0cFhKEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.parameter.3" name="perspectiveId" value="org.argeo.cms.e4.perspective.monitoring"/>
+        </children>
+        <children xsi:type="menu:ToolBarSeparator" xmi:id="_wuoL8FhLEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.toolbarseparator.0"/>
+        <children xsi:type="menu:HandledToolItem" xmi:id="_2v8DkFhKEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.handledtoolitem.logout" label="Log out" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/logout.png" command="_PsWd0FhLEeiknZQLx-vtnA"/>
+      </children>
+    </trimBars>
+  </children>
+  <handlers xmi:id="_Xp-P4CqREeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handler.0" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.jcr.handlers.AddFolderNode" command="_RgE5cCqREeidr6NYQH6GbQ"/>
+  <handlers xmi:id="_jbnNwCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handler.1" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.jcr.handlers.DeleteNodes" command="_ChJ-4CqYEeidr6NYQH6GbQ"/>
+  <handlers xmi:id="_loxB0CqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handler.2" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.jcr.handlers.Refresh" command="_TOKHsCqYEeidr6NYQH6GbQ"/>
+  <handlers xmi:id="_omPfkCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.handler.3" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.jcr.handlers.RenameNode" command="_ZrcUMCqYEeidr6NYQH6GbQ"/>
+  <handlers xmi:id="_dUg-cFgQEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.handler.9" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.jcr.handlers.AddRemoteRepository" command="_ZWpasFgQEeiknZQLx-vtnA"/>
+  <handlers xmi:id="_RQyFAFhLEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.handler.10" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.handlers.CloseWorkbench" command="_PsWd0FhLEeiknZQLx-vtnA"/>
+  <descriptors xmi:id="_XzfoMCqlEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.partdescriptor.nodeEditor" label="Node Editor" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/node.gif" allowMultiple="true" category="dataExplorer" closeable="true" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.jcr.JcrNodeEditor"/>
+  <descriptors xmi:id="_sAdNwDvdEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partdescriptor.userEditor" label="User Editor" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/person.png" allowMultiple="true" category="usersEditorArea" closeable="true" dirtyable="true" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.users.UserEditor"/>
+  <descriptors xmi:id="_5nK7EDvdEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.partdescriptor.groupEditor" label="Group Editor" iconURI="platform:/plugin/org.argeo.cms.ui.theme/icons/group.png" allowMultiple="true" category="usersEditorArea" closeable="true" dirtyable="true" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.users.GroupEditor"/>
+  <commands xmi:id="_RgE5cCqREeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.command.addFolderNode" commandName="Add folder node" category="_MDkwUCqYEeidr6NYQH6GbQ"/>
+  <commands xmi:id="_ChJ-4CqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.command.deleteNodes" commandName="Delete nodes" category="_MDkwUCqYEeidr6NYQH6GbQ"/>
+  <commands xmi:id="_TOKHsCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.command.refreshNodes" commandName="Refresh nodes" category="_MDkwUCqYEeidr6NYQH6GbQ"/>
+  <commands xmi:id="_ZrcUMCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.command.renameNode" commandName="Rename node" category="_MDkwUCqYEeidr6NYQH6GbQ"/>
+  <commands xmi:id="_uL5i4DvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.command.add" commandName="Add"/>
+  <commands xmi:id="_xkcMADvjEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.command.delete" commandName="Delete"/>
+  <commands xmi:id="_AF1UsDvrEeiF1foPJZSZkw" elementId="org.argeo.cms.e4.command.openPerspective" commandName="Open Perspective">
+    <parameters xmi:id="_F3WAUDvrEeiF1foPJZSZkw" elementId="perspectiveId" name="Perspective Id" optional="false"/>
+  </commands>
+  <commands xmi:id="_ZWpasFgQEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.command.addRemoteRepository" commandName="Add Remote Repository"/>
+  <commands xmi:id="_PsWd0FhLEeiknZQLx-vtnA" elementId="org.argeo.cms.e4.command.logout" commandName="Log out"/>
+  <addons xmi:id="_XqkCQaknEeObFrG_clJBYA" elementId="org.eclipse.e4.core.commands.service" contributionURI="bundleclass://org.eclipse.e4.core.commands/org.eclipse.e4.core.commands.CommandServiceAddon"/>
+  <addons xmi:id="_XqkCQqknEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.contexts.service" contributionURI="bundleclass://org.eclipse.e4.ui.services/org.eclipse.e4.ui.services.ContextServiceAddon"/>
+  <addons xmi:id="_XqkCQ6knEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.bindings.service" contributionURI="bundleclass://org.eclipse.e4.ui.bindings/org.eclipse.e4.ui.bindings.BindingServiceAddon"/>
+  <addons xmi:id="_XqkCRKknEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.workbench.commands.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.CommandProcessingAddon"/>
+  <addons xmi:id="_XqkCRaknEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.workbench.contexts.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.ContextProcessingAddon"/>
+  <addons xmi:id="_XqkCRqknEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.workbench.bindings.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench.swt/org.eclipse.e4.ui.workbench.swt.util.BindingProcessingAddon"/>
+  <addons xmi:id="_XqkCR6knEeObFrG_clJBYA" elementId="org.eclipse.e4.ui.workbench.handler.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.HandlerProcessingAddon"/>
+  <addons xmi:id="_8VnK8OdKEeijEOqYKRSeoQ" elementId="org.argeo.cms.e4.addon.0" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.addons.LocaleAddon"/>
+  <addons xmi:id="_-xeJYOdKEeijEOqYKRSeoQ" elementId="org.argeo.cms.e4.addon.1" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.addons.AuthAddon"/>
+  <categories xmi:id="_MDkwUCqYEeidr6NYQH6GbQ" elementId="org.argeo.cms.e4.category.jcrBrowser" name="JCR Browser"/>
+</application:Application>
diff --git a/org.argeo.cms.e4/pom.xml b/org.argeo.cms.e4/pom.xml
new file mode 100644 (file)
index 0000000..e3192d5
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.cms.e4</artifactId>
+       <name>CMS E4</name>
+       <packaging>jar</packaging>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- UI -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp</groupId>
+                       <artifactId>argeo-tp-rap-e4</artifactId>
+                       <version>${version.argeo-tp}</version>
+                       <type>pom</type>
+                       <scope>provided</scope>
+               </dependency>
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/CmsE4Utils.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/CmsE4Utils.java
new file mode 100644 (file)
index 0000000..b8ad37e
--- /dev/null
@@ -0,0 +1,28 @@
+package org.argeo.cms.e4;
+
+import org.argeo.cms.CmsException;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.EPartService.PartState;
+
+public class CmsE4Utils {
+       public static void openEditor(EPartService partService, String editorId, String key, String state) {
+               for (MPart part : partService.getParts()) {
+                       String id = part.getPersistedState().get(key);
+                       if (id != null && state.equals(id)) {
+                               partService.showPart(part, PartState.ACTIVATE);
+                               return;
+                       }
+               }
+
+               // new part
+               MPart part = partService.createPart(editorId);
+               if (part == null)
+                       throw new CmsException("No editor found with id " + editorId);
+               part.getPersistedState().put(key, state);
+               partService.showPart(part, PartState.ACTIVATE);
+       }
+
+       private CmsE4Utils() {
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/PrivilegedJob.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/PrivilegedJob.java
new file mode 100644 (file)
index 0000000..89055d2
--- /dev/null
@@ -0,0 +1,49 @@
+package org.argeo.cms.e4;
+
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+import javax.security.auth.Subject;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.jobs.Job;
+
+/**
+ * Propagate authentication to an eclipse job. Typically to execute a privileged
+ * action outside the UI thread
+ */
+public abstract class PrivilegedJob extends Job {
+       private final Subject subject;
+
+       public PrivilegedJob(String jobName) {
+               this(jobName, AccessController.getContext());
+       }
+
+       public PrivilegedJob(String jobName,
+                       AccessControlContext accessControlContext) {
+               super(jobName);
+               subject = Subject.getSubject(accessControlContext);
+
+               // Must be called *before* the job is scheduled,
+               // it is required for the progress window to appear
+               setUser(true);
+       }
+
+       @Override
+       protected IStatus run(final IProgressMonitor progressMonitor) {
+               PrivilegedAction<IStatus> privilegedAction = new PrivilegedAction<IStatus>() {
+                       public IStatus run() {
+                               return doRun(progressMonitor);
+                       }
+               };
+               return Subject.doAs(subject, privilegedAction);
+       }
+
+       /**
+        * Implement here what should be executed with default context
+        * authentication
+        */
+       protected abstract IStatus doRun(IProgressMonitor progressMonitor);
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/addons/AuthAddon.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/addons/AuthAddon.java
new file mode 100644 (file)
index 0000000..326a67e
--- /dev/null
@@ -0,0 +1,105 @@
+package org.argeo.cms.e4.addons;
+
+import java.security.AccessController;
+import java.util.Iterator;
+
+import javax.annotation.PostConstruct;
+import javax.security.auth.Subject;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.eclipse.e4.ui.model.application.MApplication;
+import org.eclipse.e4.ui.model.application.ui.MElementContainer;
+import org.eclipse.e4.ui.model.application.ui.MUIElement;
+import org.eclipse.e4.ui.model.application.ui.basic.MTrimBar;
+import org.eclipse.e4.ui.model.application.ui.basic.MTrimmedWindow;
+import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
+
+public class AuthAddon {
+       private final static Log log = LogFactory.getLog(AuthAddon.class);
+
+       public final static String AUTH = "auth.";
+
+       @PostConstruct
+       void init(MApplication application) {
+               Iterator<MWindow> windows = application.getChildren().iterator();
+               boolean atLeastOneTopLevelWindowVisible = false;
+               windows: while (windows.hasNext()) {
+                       MWindow window = windows.next();
+                       // main window
+                       boolean windowVisible = process(window);
+                       if (!windowVisible) {
+//                             windows.remove();
+                               continue windows;
+                       }
+                       atLeastOneTopLevelWindowVisible = true;
+                       // trim bars
+                       if (window instanceof MTrimmedWindow) {
+                               Iterator<MTrimBar> trimBars = ((MTrimmedWindow) window).getTrimBars().iterator();
+                               while (trimBars.hasNext()) {
+                                       MTrimBar trimBar = trimBars.next();
+                                       if (!process(trimBar)) {
+                                               trimBars.remove();
+                                       }
+                               }
+                       }
+               }
+
+               if (!atLeastOneTopLevelWindowVisible) {
+                       log.warn("No top-level window is authorized for user " + CurrentUser.getUsername() + ", logging out..");
+                       logout();
+               }
+       }
+
+       protected boolean process(MUIElement element) {
+               for (String tag : element.getTags()) {
+                       if (tag.startsWith(AUTH)) {
+                               String role = tag.substring(AUTH.length(), tag.length());
+                               if (!CurrentUser.isInRole(role)) {
+                                       element.setVisible(false);
+                                       element.setToBeRendered(false);
+                                       return false;
+                               }
+                       }
+               }
+
+               // children
+               if (element instanceof MElementContainer) {
+                       @SuppressWarnings("unchecked")
+                       MElementContainer<? extends MUIElement> container = (MElementContainer<? extends MUIElement>) element;
+                       Iterator<? extends MUIElement> children = container.getChildren().iterator();
+                       while (children.hasNext()) {
+                               MUIElement child = children.next();
+                               boolean visible = process(child);
+                               if (!visible)
+                                       children.remove();
+                       }
+
+                       for (Object child : container.getChildren()) {
+                               if (child instanceof MUIElement) {
+                                       boolean visible = process((MUIElement) child);
+                                       if (!visible)
+                                               container.getChildren().remove(child);
+                               }
+                       }
+               }
+
+               return true;
+       }
+
+       protected void logout() {
+               Subject subject = Subject.getSubject(AccessController.getContext());
+               try {
+                       CurrentUser.logoutCmsSession(subject);
+               } catch (Exception e) {
+                       throw new CmsException("Cannot log out", e);
+               }
+               HttpServletRequest request = org.argeo.eclipse.ui.specific.UiContext.getHttpRequest();
+               if (request != null)
+                       request.getSession().setMaxInactiveInterval(0);
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/addons/LocaleAddon.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/addons/LocaleAddon.java
new file mode 100644 (file)
index 0000000..5bc0d69
--- /dev/null
@@ -0,0 +1,51 @@
+package org.argeo.cms.e4.addons;
+
+import java.security.AccessController;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+import javax.annotation.PostConstruct;
+import javax.security.auth.Subject;
+
+import org.argeo.eclipse.ui.specific.UiContext;
+import org.eclipse.e4.core.services.nls.ILocaleChangeService;
+import org.eclipse.e4.ui.model.application.MApplication;
+import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
+import org.eclipse.e4.ui.workbench.modeling.EModelService;
+import org.eclipse.e4.ui.workbench.modeling.ElementMatcher;
+import org.eclipse.swt.SWT;
+
+/** Integrate workbench with the locale provided at log in. */
+public class LocaleAddon {
+       private final static String STYLE_OVERRIDE = "styleOverride";
+
+       // Right to left languages
+       private final static String ARABIC = "ar";
+       private final static String HEBREW = "he";
+
+       @PostConstruct
+       public void init(ILocaleChangeService localeChangeService, EModelService modelService, MApplication application) {
+               Subject subject = Subject.getSubject(AccessController.getContext());
+               Set<Locale> locales = subject.getPublicCredentials(Locale.class);
+               if (!locales.isEmpty()) {
+                       Locale locale = locales.iterator().next();
+                       localeChangeService.changeApplicationLocale(locale);
+                       UiContext.setLocale(locale);
+
+                       if (locale.getLanguage().equals(ARABIC) || locale.getLanguage().equals(HEBREW)) {
+                               List<MWindow> windows = modelService.findElements(application, MWindow.class, EModelService.ANYWHERE,
+                                               new ElementMatcher(null, null, (String) null));
+                               for (MWindow window : windows) {
+                                       String currentStyle = window.getPersistedState().get(STYLE_OVERRIDE);
+                                       int style = 0;
+                                       if (currentStyle != null) {
+                                               style = Integer.parseInt(currentStyle);
+                                       }
+                                       style = style | SWT.RIGHT_TO_LEFT;
+                                       window.getPersistedState().put(STYLE_OVERRIDE, Integer.toString(style));
+                               }
+                       }
+               }
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/contexts/OsgiFilterContextFunction.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/contexts/OsgiFilterContextFunction.java
new file mode 100644 (file)
index 0000000..b0fdcc1
--- /dev/null
@@ -0,0 +1,33 @@
+package org.argeo.cms.e4.contexts;
+
+import org.argeo.cms.CmsException;
+import org.eclipse.e4.core.contexts.ContextFunction;
+import org.eclipse.e4.core.contexts.IEclipseContext;
+import org.eclipse.e4.core.di.IInjector;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+@SuppressWarnings("restriction")
+public class OsgiFilterContextFunction extends ContextFunction {
+
+       private BundleContext bc = FrameworkUtil.getBundle(OsgiFilterContextFunction.class).getBundleContext();
+
+       @Override
+       public Object compute(IEclipseContext context, String contextKey) {
+               ServiceReference<?>[] srs;
+               try {
+                       srs = bc.getServiceReferences((String) null, contextKey);
+               } catch (InvalidSyntaxException e) {
+                       throw new CmsException("Context key " + contextKey + " must be a valid osgi filter", e);
+               }
+               if (srs == null || srs.length == 0) {
+                       return IInjector.NOT_A_VALUE;
+               } else {
+                       // return the first one
+                       return bc.getService(srs[0]);
+               }
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/files/NodeFsBrowserView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/files/NodeFsBrowserView.java
new file mode 100644 (file)
index 0000000..5b79aaf
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.files;
+
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.spi.FileSystemProvider;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+
+import org.argeo.cms.CmsException;
+import org.argeo.eclipse.ui.fs.AdvancedFsBrowser;
+import org.argeo.eclipse.ui.fs.SimpleFsBrowser;
+import org.argeo.node.NodeUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+
+/** Browse the node file system. */
+public class NodeFsBrowserView {
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID +
+       // ".nodeFsBrowserView";
+
+       @Inject
+       FileSystemProvider nodeFileSystemProvider;
+
+       @PostConstruct
+       public void createPartControl(Composite parent) {
+               try {
+                       //URI uri = new URI("node://root:demo@localhost:7070/");
+                       URI uri = new URI("node:///");
+                       FileSystem fileSystem = nodeFileSystemProvider.getFileSystem(uri);
+                       if (fileSystem == null)
+                               fileSystem = nodeFileSystemProvider.newFileSystem(uri, null);
+                       Path nodePath = fileSystem.getPath("~");
+
+                       Path localPath = Paths.get(System.getProperty("user.home"));
+
+                       SimpleFsBrowser browser = new SimpleFsBrowser(parent, SWT.NO_FOCUS);
+                       browser.setInput(nodePath, localPath);
+//                     AdvancedFsBrowser browser = new AdvancedFsBrowser();
+//                     browser.createUi(parent, localPath);
+               } catch (Exception e) {
+                       throw new CmsException("Cannot open file system browser", e);
+               }
+       }
+
+       public void setFocus() {
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangeLanguage.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangeLanguage.java
new file mode 100644 (file)
index 0000000..416df7d
--- /dev/null
@@ -0,0 +1,13 @@
+package org.argeo.cms.e4.handlers;
+
+import java.util.Locale;
+
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.core.services.nls.ILocaleChangeService;
+
+public class ChangeLanguage {
+       @Execute
+       public void execute(ILocaleChangeService localeChangeService) {
+               localeChangeService.changeApplicationLocale(Locale.FRENCH);
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangePassword.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/ChangePassword.java
new file mode 100644 (file)
index 0000000..3784093
--- /dev/null
@@ -0,0 +1,138 @@
+package org.argeo.cms.e4.handlers;
+
+import static org.argeo.cms.CmsMsg.changePassword;
+import static org.argeo.cms.CmsMsg.currentPassword;
+import static org.argeo.cms.CmsMsg.newPassword;
+import static org.argeo.cms.CmsMsg.passwordChanged;
+import static org.argeo.cms.CmsMsg.repeatNewPassword;
+
+import java.security.AccessController;
+import java.util.Arrays;
+
+import javax.inject.Inject;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+import javax.transaction.UserTransaction;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.dialogs.CmsMessageDialog;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.node.security.CryptoKeyring;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+public class ChangePassword {
+       @Inject
+       private UserAdmin userAdmin;
+       @Inject
+       private UserTransaction userTransaction;
+       @Inject
+       private CryptoKeyring keyring = null;
+
+       @Execute
+       public void execute() {
+               ChangePasswordDialog dialog = new ChangePasswordDialog(Display.getCurrent().getActiveShell(), userAdmin);
+               if (dialog.open() == Dialog.OK) {
+                       new CmsMessageDialog(Display.getCurrent().getActiveShell(), passwordChanged.lead(),
+                                       CmsMessageDialog.INFORMATION).open();
+               }
+       }
+
+       protected void changePassword(char[] oldPassword, char[] newPassword) {
+               Subject subject = Subject.getSubject(AccessController.getContext());
+               String name = subject.getPrincipals(X500Principal.class).iterator().next().toString();
+               LdapName dn;
+               try {
+                       dn = new LdapName(name);
+               } catch (InvalidNameException e) {
+                       throw new CmsException("Invalid user dn " + name, e);
+               }
+               User user = (User) userAdmin.getRole(dn.toString());
+               if (!user.hasCredential(null, oldPassword))
+                       throw new CmsException("Invalid password");
+               if (Arrays.equals(newPassword, new char[0]))
+                       throw new CmsException("New password empty");
+               try {
+                       userTransaction.begin();
+                       user.getCredentials().put(null, newPassword);
+                       if (keyring != null) {
+                               keyring.changePassword(oldPassword, newPassword);
+                               // TODO change secret keys in the CMS session
+                       }
+                       userTransaction.commit();
+               } catch (Exception e) {
+                       try {
+                               userTransaction.rollback();
+                       } catch (Exception e1) {
+                               e1.printStackTrace();
+                       }
+                       if (e instanceof RuntimeException)
+                               throw (RuntimeException) e;
+                       else
+                               throw new CmsException("Cannot change password", e);
+               }
+       }
+
+       class ChangePasswordDialog extends CmsMessageDialog {
+               private Text oldPassword, newPassword1, newPassword2;
+
+               public ChangePasswordDialog(Shell parentShell, UserAdmin securityService) {
+                       super(parentShell, changePassword.lead(), CONFIRM);
+               }
+
+               protected Point getInitialSize() {
+                       return new Point(400, 450);
+               }
+
+               protected Control createDialogArea(Composite parent) {
+                       Composite dialogarea = (Composite) super.createDialogArea(parent);
+                       dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       Composite composite = new Composite(dialogarea, SWT.NONE);
+                       composite.setLayout(new GridLayout(2, false));
+                       composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+                       oldPassword = createLP(composite, currentPassword.lead());
+                       newPassword1 = createLP(composite, newPassword.lead());
+                       newPassword2 = createLP(composite, repeatNewPassword.lead());
+
+                       parent.pack();
+                       oldPassword.setFocus();
+                       return composite;
+               }
+
+               @Override
+               protected void okPressed() {
+                       try {
+                               if (!newPassword1.getText().equals(newPassword2.getText()))
+                                       throw new CmsException("New passwords are different");
+                               changePassword(oldPassword.getTextChars(), newPassword1.getTextChars());
+                               closeShell(OK);
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot change password", e);
+                       }
+               }
+
+               /** Creates label and password. */
+               protected Text createLP(Composite parent, String label) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.PASSWORD | SWT.BORDER);
+                       text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+                       return text;
+               }
+
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseAllParts.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseAllParts.java
new file mode 100644 (file)
index 0000000..d11c041
--- /dev/null
@@ -0,0 +1,37 @@
+package org.argeo.cms.e4.handlers;
+
+import org.eclipse.e4.core.di.annotations.CanExecute;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+
+public class CloseAllParts {
+
+       @Execute
+       void execute(EPartService partService) {
+               for (MPart part : partService.getParts()) {
+                       if (part.isCloseable()) {
+                               if (part.isDirty()) {
+                                       if (partService.savePart(part, true)) {
+                                               partService.hidePart(part, true);
+                                       }
+                               } else {
+                                       partService.hidePart(part, true);
+                               }
+                       }
+               }
+       }
+
+       @CanExecute
+       boolean canExecute(EPartService partService) {
+               boolean atLeastOnePart = false;
+               for (MPart part : partService.getParts()) {
+                       if (part.isVisible() && part.isCloseable()) {
+                               atLeastOnePart = true;
+                               break;
+                       }
+               }
+               return atLeastOnePart;
+       }
+
+}
\ No newline at end of file
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseWorkbench.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/CloseWorkbench.java
new file mode 100644 (file)
index 0000000..a365f3d
--- /dev/null
@@ -0,0 +1,28 @@
+package org.argeo.cms.e4.handlers;
+
+import java.security.AccessController;
+
+import javax.security.auth.Subject;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.workbench.IWorkbench;
+
+public class CloseWorkbench {
+       @Execute
+       public void execute(IWorkbench workbench) {
+               logout();
+               workbench.close();
+       }
+
+       protected void logout() {
+               Subject subject = Subject.getSubject(AccessController.getContext());
+               try {
+                       CurrentUser.logoutCmsSession(subject);
+               } catch (Exception e) {
+                       throw new CmsException("Cannot log out", e);
+               }
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/DoNothing.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/DoNothing.java
new file mode 100644 (file)
index 0000000..358494c
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.cms.e4.handlers;
+
+import org.eclipse.e4.core.di.annotations.Execute;
+
+public class DoNothing {
+       @Execute
+       public void execute() {
+
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/LanguageMenuContribution.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/LanguageMenuContribution.java
new file mode 100644 (file)
index 0000000..ac825bb
--- /dev/null
@@ -0,0 +1,29 @@
+
+package org.argeo.cms.e4.handlers;
+
+import java.util.Date;
+import java.util.List;
+
+import org.eclipse.e4.ui.di.AboutToHide;
+import org.eclipse.e4.ui.di.AboutToShow;
+import org.eclipse.e4.ui.model.application.ui.menu.MDirectMenuItem;
+import org.eclipse.e4.ui.model.application.ui.menu.MMenuElement;
+import org.eclipse.e4.ui.workbench.modeling.EModelService;
+
+public class LanguageMenuContribution {
+       @AboutToShow
+       public void aboutToShow(List<MMenuElement> items, EModelService modelService) {
+               MDirectMenuItem dynamicItem = modelService.createModelElement(MDirectMenuItem.class);
+               dynamicItem.setLabel("Dynamic Menu Item (" + new Date() + ")");
+               //dynamicItem.setContributorURI("platform:/plugin/org.argeo.cms.e4");
+               //dynamicItem.setContributionURI("bundleclass://org.argeo.cms.e4/" + ChangeLanguage.class.getName());
+               dynamicItem.setEnabled(true);
+               dynamicItem.setContributionURI("bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.handlers.ChangeLanguage");
+               items.add(dynamicItem);
+       }
+
+       @AboutToHide
+       public void aboutToHide() {
+               
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/OpenPerspective.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/OpenPerspective.java
new file mode 100644 (file)
index 0000000..ac544b1
--- /dev/null
@@ -0,0 +1,31 @@
+package org.argeo.cms.e4.handlers;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.MApplication;
+import org.eclipse.e4.ui.model.application.ui.advanced.MPerspective;
+import org.eclipse.e4.ui.workbench.modeling.EModelService;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+
+public class OpenPerspective {
+       @Inject
+       MApplication application;
+       @Inject
+       EPartService partService;
+       @Inject
+       EModelService modelService;
+
+       @Execute
+       public void execute(@Named("perspectiveId") String perspectiveId) {
+               List<MPerspective> perspectives = modelService.findElements(application, perspectiveId, MPerspective.class,
+                               null);
+               if (perspectives.size() == 0)
+                       return;
+               MPerspective perspective = perspectives.get(0);
+               partService.switchPerspective(perspective);
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/SaveAllParts.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/SaveAllParts.java
new file mode 100644 (file)
index 0000000..3b60abd
--- /dev/null
@@ -0,0 +1,19 @@
+package org.argeo.cms.e4.handlers;
+
+import org.eclipse.e4.core.di.annotations.CanExecute;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+
+public class SaveAllParts {
+
+       @Execute
+       void execute(EPartService partService) {
+               partService.saveAll(false);
+       }
+
+       @CanExecute
+       boolean canExecute(EPartService partService) {
+               return partService.getDirtyParts().size() > 0;
+       }
+
+}
\ No newline at end of file
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/SavePart.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/handlers/SavePart.java
new file mode 100644 (file)
index 0000000..73486f3
--- /dev/null
@@ -0,0 +1,18 @@
+package org.argeo.cms.e4.handlers;
+
+import org.eclipse.e4.core.di.annotations.CanExecute;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+
+public class SavePart {
+       @Execute
+       void execute(EPartService partService, MPart part) {
+               partService.savePart(part, false);
+       }
+
+       @CanExecute
+       boolean canExecute(MPart part) {
+               return part.isDirty();
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/GenericPropertyPage.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/GenericPropertyPage.java
new file mode 100644 (file)
index 0000000..b0e5cb0
--- /dev/null
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.jcr.PropertyLabelProvider;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.layout.TreeColumnLayout;
+import org.eclipse.jface.viewers.ColumnWeightData;
+import org.eclipse.jface.viewers.IBaseLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+/**
+ * Generic editor property page. Lists all properties of current node as a
+ * complex tree. TODO: enable editing
+ */
+public class GenericPropertyPage {
+
+       // Main business Objects
+       private Node currentNode;
+
+       public GenericPropertyPage(Node currentNode) {
+               this.currentNode = currentNode;
+       }
+
+       protected void createFormContent(Composite parent) {
+               Composite innerBox = new Composite(parent, SWT.NONE);
+               // Composite innerBox = new Composite(body, SWT.NO_FOCUS);
+               FillLayout layout = new FillLayout();
+               layout.marginHeight = 5;
+               layout.marginWidth = 5;
+               innerBox.setLayout(layout);
+               createComplexTree(innerBox);
+               // TODO TreeColumnLayout triggers a scroll issue with the form:
+               // The inside body is always to big and a scroll bar is shown
+               // Composite tableCmp = new Composite(body, SWT.NO_FOCUS);
+               // createComplexTree(tableCmp);
+       }
+
+       private TreeViewer createComplexTree(Composite parent) {
+               int style = SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION;
+               Tree tree = new Tree(parent, style);
+               TreeColumnLayout tableColumnLayout = new TreeColumnLayout();
+
+               createColumn(tree, tableColumnLayout, "Property", SWT.LEFT, 200, 30);
+               createColumn(tree, tableColumnLayout, "Value(s)", SWT.LEFT, 300, 60);
+               createColumn(tree, tableColumnLayout, "Type", SWT.LEFT, 75, 10);
+               createColumn(tree, tableColumnLayout, "Attributes", SWT.LEFT, 75, 0);
+               // Do not apply the treeColumnLayout it does not work yet
+               // parent.setLayout(tableColumnLayout);
+
+               tree.setLinesVisible(true);
+               tree.setHeaderVisible(true);
+
+               TreeViewer treeViewer = new TreeViewer(tree);
+               treeViewer.setContentProvider(new TreeContentProvider());
+               treeViewer.setLabelProvider((IBaseLabelProvider) new PropertyLabelProvider());
+               treeViewer.setInput(currentNode);
+               treeViewer.expandAll();
+               return treeViewer;
+       }
+
+       private static TreeColumn createColumn(Tree parent, TreeColumnLayout tableColumnLayout, String name, int style,
+                       int width, int weight) {
+               TreeColumn column = new TreeColumn(parent, style);
+               column.setText(name);
+               column.setWidth(width);
+               column.setMoveable(true);
+               column.setResizable(true);
+               tableColumnLayout.setColumnData(column, new ColumnWeightData(weight, width, true));
+               return column;
+       }
+
+       private class TreeContentProvider implements ITreeContentProvider {
+               private static final long serialVersionUID = -6162736530019406214L;
+
+               public Object[] getElements(Object parent) {
+                       Object[] props = null;
+                       try {
+
+                               if (parent instanceof Node) {
+                                       Node node = (Node) parent;
+                                       PropertyIterator pi;
+                                       pi = node.getProperties();
+                                       List<Property> propList = new ArrayList<Property>();
+                                       while (pi.hasNext()) {
+                                               propList.add(pi.nextProperty());
+                                       }
+                                       props = propList.toArray();
+                               }
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Unexpected exception while listing node properties", e);
+                       }
+                       return props;
+               }
+
+               public Object getParent(Object child) {
+                       return null;
+               }
+
+               public Object[] getChildren(Object parent) {
+                       if (parent instanceof Property) {
+                               Property prop = (Property) parent;
+                               try {
+                                       if (prop.isMultiple())
+                                               return prop.getValues();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Cannot get multi-prop values on " + prop, e);
+                               }
+                       }
+                       return null;
+               }
+
+               public boolean hasChildren(Object parent) {
+                       try {
+                               return (parent instanceof Property && ((Property) parent).isMultiple());
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Cannot check if property is multiple for " + parent, e);
+                       }
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+
+               public void dispose() {
+               }
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/JcrBrowserView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/JcrBrowserView.java
new file mode 100644 (file)
index 0000000..7639df4
--- /dev/null
@@ -0,0 +1,360 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.jcr;
+
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.ObservationManager;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.jcr.JcrBrowserUtils;
+import org.argeo.cms.ui.jcr.NodeContentProvider;
+import org.argeo.cms.ui.jcr.NodeLabelProvider;
+import org.argeo.cms.ui.jcr.OsgiRepositoryRegister;
+import org.argeo.cms.ui.jcr.PropertiesContentProvider;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.jcr.AsyncUiEventListener;
+import org.argeo.eclipse.ui.jcr.utils.NodeViewerComparer;
+import org.argeo.node.security.CryptoKeyring;
+import org.argeo.node.security.Keyring;
+import org.eclipse.e4.core.contexts.IEclipseContext;
+import org.eclipse.e4.ui.services.EMenuService;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IBaseLabelProvider;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Basic View to display a sash form to browse a JCR compliant multiple
+ * repository environment
+ */
+public class JcrBrowserView {
+       final static String ID = "org.argeo.cms.e4.jcrbrowser";
+       final static String NODE_VIEWER_POPUP_MENU_ID = "org.argeo.cms.e4.popupmenu.nodeViewer";
+
+       private boolean sortChildNodes = true;
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       private Keyring keyring;
+       @Inject
+       private RepositoryFactory repositoryFactory;
+       @Inject
+       private Repository nodeRepository;
+
+       // Current user session on the home repository default workspace
+       private Session userSession;
+
+       private OsgiRepositoryRegister repositoryRegister = new OsgiRepositoryRegister();
+
+       // This page widgets
+       private TreeViewer nodesViewer;
+       private NodeContentProvider nodeContentProvider;
+       private TableViewer propertiesViewer;
+       private EventListener resultsObserver;
+
+       @PostConstruct
+       public void createPartControl(Composite parent, IEclipseContext context, EPartService partService,
+                       ESelectionService selectionService, EMenuService menuService) {
+               repositoryRegister.init();
+
+               parent.setLayout(new FillLayout());
+               SashForm sashForm = new SashForm(parent, SWT.VERTICAL);
+               // sashForm.setSashWidth(4);
+               // sashForm.setLayout(new FillLayout());
+
+               // Create the tree on top of the view
+               Composite top = new Composite(sashForm, SWT.NONE);
+               // GridLayout gl = new GridLayout(1, false);
+               top.setLayout(CmsUtils.noSpaceGridLayout());
+
+               try {
+                       this.userSession = this.nodeRepository.login();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot open user session", e);
+               }
+
+               nodeContentProvider = new NodeContentProvider(userSession, keyring, repositoryRegister, repositoryFactory,
+                               sortChildNodes);
+
+               // nodes viewer
+               nodesViewer = createNodeViewer(top, nodeContentProvider);
+
+               // context menu : it is completely defined in the plugin.xml file.
+               // MenuManager menuManager = new MenuManager();
+               // Menu menu = menuManager.createContextMenu(nodesViewer.getTree());
+
+               // nodesViewer.getTree().setMenu(menu);
+
+               nodesViewer.setInput("");
+
+               // Create the property viewer on the bottom
+               Composite bottom = new Composite(sashForm, SWT.NONE);
+               bottom.setLayout(CmsUtils.noSpaceGridLayout());
+               propertiesViewer = createPropertiesViewer(bottom);
+
+               sashForm.setWeights(getWeights());
+               nodesViewer.setComparer(new NodeViewerComparer());
+               nodesViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+                       public void selectionChanged(SelectionChangedEvent event) {
+                               IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+                               selectionService.setSelection(selection.toList());
+                       }
+               });
+               nodesViewer.addDoubleClickListener(new JcrE4DClickListener(nodesViewer, partService));
+               menuService.registerContextMenu(nodesViewer.getControl(), NODE_VIEWER_POPUP_MENU_ID);
+               // getSite().registerContextMenu(menuManager, nodesViewer);
+               // getSite().setSelectionProvider(nodesViewer);
+       }
+
+       @PreDestroy
+       public void dispose() {
+               repositoryRegister.destroy();
+       }
+
+       public void refresh(Object obj) {
+               // Enable full refresh from a command when no element of the tree is
+               // selected
+               if (obj == null) {
+                       Object[] elements = nodeContentProvider.getElements(null);
+                       for (Object el : elements) {
+                               if (el instanceof TreeParent)
+                                       JcrBrowserUtils.forceRefreshIfNeeded((TreeParent) el);
+                               getNodeViewer().refresh(el);
+                       }
+               } else
+                       getNodeViewer().refresh(obj);
+       }
+
+       /**
+        * To be overridden to adapt size of form and result frames.
+        */
+       protected int[] getWeights() {
+               return new int[] { 70, 30 };
+       }
+
+       protected TreeViewer createNodeViewer(Composite parent, final ITreeContentProvider nodeContentProvider) {
+
+               final TreeViewer tmpNodeViewer = new TreeViewer(parent, SWT.MULTI);
+
+               tmpNodeViewer.getTree().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               tmpNodeViewer.setContentProvider(nodeContentProvider);
+               tmpNodeViewer.setLabelProvider((IBaseLabelProvider) new NodeLabelProvider());
+               tmpNodeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+                       public void selectionChanged(SelectionChangedEvent event) {
+                               if (!event.getSelection().isEmpty()) {
+                                       IStructuredSelection sel = (IStructuredSelection) event.getSelection();
+                                       Object firstItem = sel.getFirstElement();
+                                       if (firstItem instanceof SingleJcrNodeElem)
+                                               propertiesViewer.setInput(((SingleJcrNodeElem) firstItem).getNode());
+                               } else {
+                                       propertiesViewer.setInput("");
+                               }
+                       }
+               });
+
+               resultsObserver = new TreeObserver(tmpNodeViewer.getTree().getDisplay());
+               if (keyring != null)
+                       try {
+                               ObservationManager observationManager = userSession.getWorkspace().getObservationManager();
+                               observationManager.addEventListener(resultsObserver, Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED, "/",
+                                               true, null, null, false);
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Cannot register listeners", e);
+                       }
+
+               // tmpNodeViewer.addDoubleClickListener(new JcrDClickListener(tmpNodeViewer));
+               return tmpNodeViewer;
+       }
+
+       protected TableViewer createPropertiesViewer(Composite parent) {
+               propertiesViewer = new TableViewer(parent, SWT.NONE);
+               propertiesViewer.getTable().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               propertiesViewer.getTable().setHeaderVisible(true);
+               propertiesViewer.setContentProvider(new PropertiesContentProvider());
+               TableViewerColumn col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Name");
+               col.getColumn().setWidth(200);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -6684361063107478595L;
+
+                       public String getText(Object element) {
+                               try {
+                                       return ((Property) element).getName();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unexpected exception in label provider", e);
+                               }
+                       }
+               });
+               col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Value");
+               col.getColumn().setWidth(400);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -8201994187693336657L;
+
+                       public String getText(Object element) {
+                               try {
+                                       Property property = (Property) element;
+                                       if (property.getType() == PropertyType.BINARY)
+                                               return "<binary>";
+                                       else if (property.isMultiple()) {
+                                               StringBuffer buf = new StringBuffer("[");
+                                               Value[] values = property.getValues();
+                                               for (int i = 0; i < values.length; i++) {
+                                                       if (i != 0)
+                                                               buf.append(", ");
+                                                       buf.append(values[i].getString());
+                                               }
+                                               buf.append(']');
+                                               return buf.toString();
+                                       } else
+                                               return property.getValue().getString();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unexpected exception in label provider", e);
+                               }
+                       }
+               });
+               col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Type");
+               col.getColumn().setWidth(200);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -6009599998150286070L;
+
+                       public String getText(Object element) {
+                               return JcrBrowserUtils.getPropertyTypeAsString((Property) element);
+                       }
+               });
+               propertiesViewer.setInput("");
+               return propertiesViewer;
+       }
+
+       protected TreeViewer getNodeViewer() {
+               return nodesViewer;
+       }
+
+       /**
+        * Resets the tree content provider
+        * 
+        * @param sortChildNodes
+        *            if true the content provider will use a comparer to sort nodes
+        *            that might slow down the display
+        */
+       public void setSortChildNodes(boolean sortChildNodes) {
+               this.sortChildNodes = sortChildNodes;
+               ((NodeContentProvider) nodesViewer.getContentProvider()).setSortChildren(sortChildNodes);
+               nodesViewer.setInput("");
+       }
+
+       /** Notifies the current view that a node has been added */
+       public void nodeAdded(TreeParent parentNode) {
+               // insure that Ui objects have been correctly created:
+               JcrBrowserUtils.forceRefreshIfNeeded(parentNode);
+               getNodeViewer().refresh(parentNode);
+               getNodeViewer().expandToLevel(parentNode, 1);
+       }
+
+       /** Notifies the current view that a node has been removed */
+       public void nodeRemoved(TreeParent parentNode) {
+               IStructuredSelection newSel = new StructuredSelection(parentNode);
+               getNodeViewer().setSelection(newSel, true);
+               // Force refresh
+               IStructuredSelection tmpSel = (IStructuredSelection) getNodeViewer().getSelection();
+               getNodeViewer().refresh(tmpSel.getFirstElement());
+       }
+
+       class TreeObserver extends AsyncUiEventListener {
+
+               public TreeObserver(Display display) {
+                       super(display);
+               }
+
+               @Override
+               protected Boolean willProcessInUiThread(List<Event> events) throws RepositoryException {
+                       for (Event event : events) {
+                               if (getLog().isTraceEnabled())
+                                       getLog().debug("Received event " + event);
+                               String path = event.getPath();
+                               int index = path.lastIndexOf('/');
+                               String propertyName = path.substring(index + 1);
+                               if (getLog().isTraceEnabled())
+                                       getLog().debug("Concerned property " + propertyName);
+                       }
+                       return false;
+               }
+
+               protected void onEventInUiThread(List<Event> events) throws RepositoryException {
+                       if (getLog().isTraceEnabled())
+                               getLog().trace("Refresh result list");
+                       nodesViewer.refresh();
+               }
+
+       }
+
+       public boolean getSortChildNodes() {
+               return sortChildNodes;
+       }
+
+       public void setFocus() {
+               getNodeViewer().getTree().setFocus();
+       }
+
+       /* DEPENDENCY INJECTION */
+       // public void setRepositoryRegister(RepositoryRegister repositoryRegister) {
+       // this.repositoryRegister = repositoryRegister;
+       // }
+
+       public void setKeyring(CryptoKeyring keyring) {
+               this.keyring = keyring;
+       }
+
+       public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
+               this.repositoryFactory = repositoryFactory;
+       }
+
+       public void setNodeRepository(Repository nodeRepository) {
+               this.nodeRepository = nodeRepository;
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/JcrE4DClickListener.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/JcrE4DClickListener.java
new file mode 100644 (file)
index 0000000..ad6a547
--- /dev/null
@@ -0,0 +1,36 @@
+package org.argeo.cms.e4.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.jcr.JcrDClickListener;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.EPartService.PartState;
+import org.eclipse.jface.viewers.TreeViewer;
+
+public class JcrE4DClickListener extends JcrDClickListener {
+       EPartService partService;
+
+       public JcrE4DClickListener(TreeViewer nodeViewer, EPartService partService) {
+               super(nodeViewer);
+               this.partService = partService;
+       }
+
+       @Override
+       protected void openNode(Node node) {
+               MPart part = partService.createPart(JcrNodeEditor.DESCRIPTOR_ID);
+               try {
+                       part.setLabel(node.getName());
+                       part.getPersistedState().put("nodeWorkspace", node.getSession().getWorkspace().getName());
+                       part.getPersistedState().put("nodePath", node.getPath());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot open " + node, e);
+               }
+
+               // the provided part is be shown
+               partService.showPart(part, PartState.ACTIVATE);
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/JcrNodeEditor.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/JcrNodeEditor.java
new file mode 100644 (file)
index 0000000..ae2b325
--- /dev/null
@@ -0,0 +1,26 @@
+package org.argeo.cms.e4.jcr;
+
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.jcr.Node;
+
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+
+public class JcrNodeEditor {
+       final static String DESCRIPTOR_ID = "org.argeo.cms.e4.partdescriptor.nodeEditor";
+
+       @PostConstruct
+       public void createUi(Composite parent, MPart part, ESelectionService selectionService) {
+               parent.setLayout(new FillLayout());
+               List<?> selection = (List<?>) selectionService.getSelection();
+               Node node = ((SingleJcrNodeElem) selection.get(0)).getNode();
+               GenericPropertyPage propertyPage = new GenericPropertyPage(node);
+               propertyPage.createFormContent(parent);
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/SimplePart.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/SimplePart.java
new file mode 100644 (file)
index 0000000..17d8d2a
--- /dev/null
@@ -0,0 +1,19 @@
+package org.argeo.cms.e4.jcr;
+
+import javax.annotation.PostConstruct;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+
+public class SimplePart {
+
+       @PostConstruct
+       void init(Composite parent) {
+               parent.setLayout(new GridLayout());
+               Label label = new Label(parent, SWT.NONE);
+               label.setText("Hello e4 World");
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/AddFolderNode.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/AddFolderNode.java
new file mode 100644 (file)
index 0000000..81afa61
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.jcr.handlers;
+
+import java.util.List;
+
+import javax.inject.Named;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.e4.jcr.JcrBrowserView;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.eclipse.ui.dialogs.SingleValue;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+
+/**
+ * Adds a node of type nt:folder, only on {@link SingleJcrNodeElem} and
+ * {@link WorkspaceElem} TreeObject types.
+ * 
+ * This handler assumes that a selection provider is available and picks only
+ * first selected item. It is UI's job to enable the command only when the
+ * selection contains one and only one element. Thus no parameter is passed
+ * through the command.
+ */
+public class AddFolderNode {
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               List<?> selection = (List<?>) selectionService.getSelection();
+               JcrBrowserView view = (JcrBrowserView) part.getObject();
+
+               if (selection != null && selection.size() == 1) {
+                       TreeParent treeParentNode = null;
+                       Node jcrParentNode = null;
+                       Object obj = selection.get(0);
+
+                       if (obj instanceof SingleJcrNodeElem) {
+                               treeParentNode = (TreeParent) obj;
+                               jcrParentNode = ((SingleJcrNodeElem) treeParentNode).getNode();
+                       } else if (obj instanceof WorkspaceElem) {
+                               treeParentNode = (TreeParent) obj;
+                               jcrParentNode = ((WorkspaceElem) treeParentNode).getRootNode();
+                       } else
+                               return;
+
+                       String folderName = SingleValue.ask("Folder name", "Enter folder name");
+                       if (folderName != null) {
+                               try {
+                                       jcrParentNode.addNode(folderName, NodeType.NT_FOLDER);
+                                       jcrParentNode.getSession().save();
+                                       view.nodeAdded(treeParentNode);
+                               } catch (RepositoryException e) {
+                                       ErrorFeedback.show("Cannot create folder " + folderName + " under " + treeParentNode, e);
+                               }
+                       }
+               } else {
+                       // ErrorFeedback.show(WorkbenchUiPlugin
+                       // .getMessage("errorUnvalidNtFolderNodeType"));
+                       ErrorFeedback.show("Invalid NT folder node type");
+               }
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/AddRemoteRepository.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/AddRemoteRepository.java
new file mode 100644 (file)
index 0000000..e51c104
--- /dev/null
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.jcr.handlers;
+
+import java.net.URI;
+import java.util.Hashtable;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.cms.e4.jcr.JcrBrowserView;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeUtils;
+import org.argeo.node.security.Keyring;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Connect to a remote repository and, if successful publish it as an OSGi
+ * service.
+ */
+public class AddRemoteRepository implements ArgeoNames {
+
+       @Inject
+       private RepositoryFactory repositoryFactory;
+       @Inject
+       private Repository nodeRepository;
+       @Inject
+       private Keyring keyring;
+
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part) {
+               JcrBrowserView view = (JcrBrowserView) part.getObject();
+               RemoteRepositoryLoginDialog dlg = new RemoteRepositoryLoginDialog(Display.getDefault().getActiveShell());
+               if (dlg.open() == Dialog.OK) {
+                       view.refresh(null);
+               }
+       }
+
+       // public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
+       // this.repositoryFactory = repositoryFactory;
+       // }
+       //
+       // public void setKeyring(Keyring keyring) {
+       // this.keyring = keyring;
+       // }
+       //
+       // public void setNodeRepository(Repository nodeRepository) {
+       // this.nodeRepository = nodeRepository;
+       // }
+
+       class RemoteRepositoryLoginDialog extends TitleAreaDialog {
+               private static final long serialVersionUID = 2234006887750103399L;
+               private Text name;
+               private Text uri;
+               private Text username;
+               private Text password;
+               private Button saveInKeyring;
+
+               public RemoteRepositoryLoginDialog(Shell parentShell) {
+                       super(parentShell);
+               }
+
+               protected Point getInitialSize() {
+                       return new Point(600, 400);
+               }
+
+               protected Control createDialogArea(Composite parent) {
+                       Composite dialogarea = (Composite) super.createDialogArea(parent);
+                       dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       Composite composite = new Composite(dialogarea, SWT.NONE);
+                       composite.setLayout(new GridLayout(2, false));
+                       composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+                       setMessage("Login to remote repository", IMessageProvider.NONE);
+                       name = createLT(composite, "Name", "remoteRepository");
+                       uri = createLT(composite, "URI", "http://localhost:7070/jcr/node");
+                       username = createLT(composite, "User", "");
+                       password = createLP(composite, "Password");
+
+                       saveInKeyring = createLC(composite, "Remember password", false);
+                       parent.pack();
+                       return composite;
+               }
+
+               @Override
+               protected void createButtonsForButtonBar(Composite parent) {
+                       super.createButtonsForButtonBar(parent);
+                       Button test = createButton(parent, 2, "Test", false);
+                       test.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = -1829962269440419560L;
+
+                               public void widgetSelected(SelectionEvent arg0) {
+                                       testConnection();
+                               }
+                       });
+               }
+
+               void testConnection() {
+                       Session session = null;
+                       try {
+                               URI checkedUri = new URI(uri.getText());
+                               String checkedUriStr = checkedUri.toString();
+
+                               Hashtable<String, String> params = new Hashtable<String, String>();
+                               params.put(NodeConstants.LABELED_URI, checkedUriStr);
+                               Repository repository = repositoryFactory.getRepository(params);
+                               if (username.getText().trim().equals("")) {// anonymous
+                                       // FIXME make it more generic
+                                       session = repository.login("main");
+                               } else {
+                                       // FIXME use getTextChars() when upgrading to 3.7
+                                       // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=297412
+                                       char[] pwd = password.getText().toCharArray();
+                                       SimpleCredentials sc = new SimpleCredentials(username.getText(), pwd);
+                                       session = repository.login(sc, "main");
+                                       MessageDialog.openInformation(getParentShell(), "Success",
+                                                       "Connection to '" + uri.getText() + "' successful");
+                               }
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Connection test failed for " + uri.getText(), e);
+                       } finally {
+                               JcrUtils.logoutQuietly(session);
+                       }
+               }
+
+               @Override
+               protected void okPressed() {
+                       Session nodeSession = null;
+                       try {
+                               nodeSession = nodeRepository.login();
+                               Node home = NodeUtils.getUserHome(nodeSession);
+
+                               Node remote = home.hasNode(ARGEO_REMOTE) ? home.getNode(ARGEO_REMOTE) : home.addNode(ARGEO_REMOTE);
+                               if (remote.hasNode(name.getText()))
+                                       throw new EclipseUiException("There is already a remote repository named " + name.getText());
+                               Node remoteRepository = remote.addNode(name.getText(), ArgeoTypes.ARGEO_REMOTE_REPOSITORY);
+                               remoteRepository.setProperty(ARGEO_URI, uri.getText());
+                               remoteRepository.setProperty(ARGEO_USER_ID, username.getText());
+                               nodeSession.save();
+                               if (saveInKeyring.getSelection()) {
+                                       String pwdPath = remoteRepository.getPath() + '/' + ARGEO_PASSWORD;
+                                       keyring.set(pwdPath, password.getText().toCharArray());
+                               }
+                               nodeSession.save();
+                               MessageDialog.openInformation(getParentShell(), "Repository Added",
+                                               "Remote repository '" + username.getText() + "@" + uri.getText() + "' added");
+
+                               super.okPressed();
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot add remote repository", e);
+                       } finally {
+                               JcrUtils.logoutQuietly(nodeSession);
+                       }
+               }
+
+               /** Creates label and text. */
+               protected Text createLT(Composite parent, String label, String initial) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER);
+                       text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       text.setText(initial);
+                       return text;
+               }
+
+               /** Creates label and check. */
+               protected Button createLC(Composite parent, String label, Boolean initial) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Button check = new Button(parent, SWT.CHECK);
+                       check.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       check.setSelection(initial);
+                       return check;
+               }
+
+               protected Text createLP(Composite parent, String label) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER | SWT.PASSWORD);
+                       text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       return text;
+               }
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/DeleteNodes.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/DeleteNodes.java
new file mode 100644 (file)
index 0000000..71459fb
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.jcr.handlers;
+
+import java.util.List;
+
+import javax.inject.Named;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.e4.jcr.JcrBrowserView;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Delete the selected nodes: both in the JCR repository and in the UI view.
+ * Warning no check is done, except implementation dependent native checks,
+ * handle with care.
+ * 
+ * This handler is still 'hard linked' to a GenericJcrBrowser view to enable
+ * correct tree refresh when a node is added. This must be corrected in future
+ * versions.
+ */
+public class DeleteNodes {
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               List<?> selection = (List<?>) selectionService.getSelection();
+               if (selection == null)
+                       return;
+
+               JcrBrowserView view = (JcrBrowserView) part.getObject();
+
+               // confirmation
+               StringBuffer buf = new StringBuffer("");
+               for (Object o : selection) {
+                       SingleJcrNodeElem sjn = (SingleJcrNodeElem) o;
+                       buf.append(sjn.getName()).append(' ');
+               }
+               Boolean doRemove = MessageDialog.openConfirm(Display.getCurrent().getActiveShell(), "Confirm deletion",
+                               "Do you want to delete " + buf + "?");
+
+               // operation
+               if (doRemove) {
+                       SingleJcrNodeElem ancestor = null;
+                       WorkspaceElem rootAncestor = null;
+                       try {
+                               for (Object obj : selection) {
+                                       if (obj instanceof SingleJcrNodeElem) {
+                                               // Cache objects
+                                               SingleJcrNodeElem sjn = (SingleJcrNodeElem) obj;
+                                               TreeParent tp = (TreeParent) sjn.getParent();
+                                               Node node = sjn.getNode();
+
+                                               // Jcr Remove
+                                               node.remove();
+                                               node.getSession().save();
+                                               // UI remove
+                                               tp.removeChild(sjn);
+
+                                               // Check if the parent is the root node
+                                               if (tp instanceof WorkspaceElem)
+                                                       rootAncestor = (WorkspaceElem) tp;
+                                               else
+                                                       ancestor = getOlder(ancestor, (SingleJcrNodeElem) tp);
+                                       }
+                               }
+                               if (rootAncestor != null)
+                                       view.nodeRemoved(rootAncestor);
+                               else if (ancestor != null)
+                                       view.nodeRemoved(ancestor);
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot delete selected node ", e);
+                       }
+               }
+       }
+
+       private SingleJcrNodeElem getOlder(SingleJcrNodeElem A, SingleJcrNodeElem B) {
+               try {
+                       if (A == null)
+                               return B == null ? null : B;
+                       // Todo enhanced this method
+                       else
+                               return A.getNode().getDepth() <= B.getNode().getDepth() ? A : B;
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot find ancestor", re);
+               }
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/Refresh.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/Refresh.java
new file mode 100644 (file)
index 0000000..0b8daf4
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.jcr.handlers;
+
+import java.util.List;
+
+import javax.inject.Named;
+
+import org.argeo.cms.e4.jcr.JcrBrowserView;
+import org.argeo.cms.ui.jcr.JcrBrowserUtils;
+import org.argeo.eclipse.ui.TreeParent;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+
+/**
+ * Force the selected objects of the active view to be refreshed doing the
+ * following:
+ * <ol>
+ * <li>The model objects are recomputed</li>
+ * <li>the view is refreshed</li>
+ * </ol>
+ */
+public class Refresh {
+
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, EPartService partService,
+                       ESelectionService selectionService) {
+
+               JcrBrowserView view = (JcrBrowserView) part.getObject();
+               List<?> selection = (List<?>) selectionService.getSelection();
+
+               if (selection != null && !selection.isEmpty()) {
+                       for (Object obj : selection)
+                               if (obj instanceof TreeParent) {
+                                       TreeParent tp = (TreeParent) obj;
+                                       JcrBrowserUtils.forceRefreshIfNeeded(tp);
+                                       view.refresh(obj);
+                               }
+               } else if (view instanceof JcrBrowserView)
+                       view.refresh(null); // force full refresh
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/RenameNode.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/jcr/handlers/RenameNode.java
new file mode 100644 (file)
index 0000000..5649527
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.jcr.handlers;
+
+import java.util.List;
+
+import javax.inject.Named;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.cms.e4.jcr.JcrBrowserView;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.dialogs.SingleValue;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+
+/**
+ * Canonically call JCR Session#move(String, String) on the first element
+ * returned by HandlerUtil#getActiveWorkbenchWindow()
+ * (...getActivePage().getSelection()), if it is a {@link SingleJcrNodeElem}.
+ * The user must then fill a new name in and confirm
+ */
+public class RenameNode {
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, EPartService partService,
+                       ESelectionService selectionService) {
+               List<?> selection = (List<?>) selectionService.getSelection();
+               if (selection == null || selection.size() != 1)
+                       return;
+               JcrBrowserView view = (JcrBrowserView) part.getObject();
+
+               Object element = selection.get(0);
+               if (element instanceof SingleJcrNodeElem) {
+                       SingleJcrNodeElem sjn = (SingleJcrNodeElem) element;
+                       Node node = sjn.getNode();
+                       Session session = null;
+                       String newName = null;
+                       String oldPath = null;
+                       try {
+                               newName = SingleValue.ask("New node name", "Please provide a new name for [" + node.getName() + "]");
+                               // TODO sanity check and user feedback
+                               newName = JcrUtils.replaceInvalidChars(newName);
+                               oldPath = node.getPath();
+                               session = node.getSession();
+                               session.move(oldPath, JcrUtils.parentPath(oldPath) + "/" + newName);
+                               session.save();
+
+                               // Manually refresh the browser view. Must be enhanced
+                               view.refresh(sjn);
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Unable to rename " + node + " to " + newName, e);
+                       }
+               }
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundleNode.java
new file mode 100644 (file)
index 0000000..962ad38
--- /dev/null
@@ -0,0 +1,46 @@
+package org.argeo.cms.e4.monitoring;
+
+import org.argeo.eclipse.ui.TreeParent;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+
+/** A tree element representing a {@link Bundle} */
+class BundleNode extends TreeParent {
+       private final Bundle bundle;
+
+       public BundleNode(Bundle bundle) {
+               this(bundle, false);
+       }
+
+       @SuppressWarnings("rawtypes")
+       public BundleNode(Bundle bundle, boolean hasChildren) {
+               super(bundle.getSymbolicName());
+               this.bundle = bundle;
+
+               if (hasChildren) {
+                       // REFERENCES
+                       ServiceReference[] usedServices = bundle.getServicesInUse();
+                       if (usedServices != null) {
+                               for (ServiceReference sr : usedServices) {
+                                       if (sr != null)
+                                               addChild(new ServiceReferenceNode(sr, false));
+                               }
+                       }
+
+                       // SERVICES
+                       ServiceReference[] registeredServices = bundle
+                                       .getRegisteredServices();
+                       if (registeredServices != null) {
+                               for (ServiceReference sr : registeredServices) {
+                                       if (sr != null)
+                                               addChild(new ServiceReferenceNode(sr, true));
+                               }
+                       }
+               }
+
+       }
+
+       Bundle getBundle() {
+               return bundle;
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/BundlesView.java
new file mode 100644 (file)
index 0000000..1d4a33f
--- /dev/null
@@ -0,0 +1,129 @@
+//package org.argeo.eclipse.ui.workbench.osgi;
+//public class BundlesView {}
+
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.monitoring;
+
+import javax.annotation.PostConstruct;
+
+import org.argeo.eclipse.ui.ColumnViewerComparator;
+import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+/**
+ * Overview of the bundles as a table. Equivalent to Equinox 'ss' console
+ * command.
+ */
+public class BundlesView {
+       private final static BundleContext bc = FrameworkUtil.getBundle(BundlesView.class).getBundleContext();
+       private TableViewer viewer;
+
+       @PostConstruct
+       public void createPartControl(Composite parent) {
+               viewer = new TableViewer(parent);
+               viewer.setContentProvider(new BundleContentProvider());
+               viewer.getTable().setHeaderVisible(true);
+
+               EclipseUiSpecificUtils.enableToolTipSupport(viewer);
+
+               // ID
+               TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(30);
+               column.getColumn().setText("ID");
+               column.getColumn().setAlignment(SWT.RIGHT);
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -3122136344359358605L;
+
+                       public String getText(Object element) {
+                               return Long.toString(((Bundle) element).getBundleId());
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // State
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(18);
+               column.getColumn().setText("State");
+               column.setLabelProvider(new StateLabelProvider());
+               new ColumnViewerComparator(column);
+
+               // Symbolic name
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(250);
+               column.getColumn().setText("Symbolic Name");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -4280840684440451080L;
+
+                       public String getText(Object element) {
+                               return ((Bundle) element).getSymbolicName();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Version
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(250);
+               column.getColumn().setText("Version");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = 6871926308708629989L;
+
+                       public String getText(Object element) {
+                               Bundle bundle = (org.osgi.framework.Bundle) element;
+                               return bundle.getVersion().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               viewer.setInput(bc);
+
+       }
+
+       @Focus
+       public void setFocus() {
+               if (viewer != null)
+                       viewer.getControl().setFocus();
+       }
+
+       /** Content provider managing the array of bundles */
+       private static class BundleContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = -8533792785725875977L;
+
+               public Object[] getElements(Object inputElement) {
+                       if (inputElement instanceof BundleContext) {
+                               BundleContext bc = (BundleContext) inputElement;
+                               return bc.getBundles();
+                       }
+                       return null;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/CmsSessionsView.java
new file mode 100644 (file)
index 0000000..a85d27b
--- /dev/null
@@ -0,0 +1,189 @@
+//package org.argeo.eclipse.ui.workbench.osgi;
+//public class BundlesView {}
+
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.monitoring;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CmsSession;
+import org.argeo.eclipse.ui.ColumnViewerComparator;
+import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils;
+import org.argeo.util.LangUtils;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * Overview of the active CMS sessions.
+ */
+public class CmsSessionsView {
+       private final static BundleContext bc = FrameworkUtil.getBundle(CmsSessionsView.class).getBundleContext();
+
+       private TableViewer viewer;
+
+       @PostConstruct
+       public void createPartControl(Composite parent) {
+               viewer = new TableViewer(parent);
+               viewer.setContentProvider(new CmsSessionContentProvider());
+               viewer.getTable().setHeaderVisible(true);
+
+               EclipseUiSpecificUtils.enableToolTipSupport(viewer);
+
+               int longColWidth = 150;
+               int smallColWidth = 100;
+
+               // Display name
+               TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(longColWidth);
+               column.getColumn().setText("User");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return ((CmsSession) element).getAuthorization().toString();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return ((CmsSession) element).getUserDn().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Creation time
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("Since");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return LangUtils.since(((CmsSession) element).getCreationTime());
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return ((CmsSession) element).getCreationTime().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Username
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("Username");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               LdapName userDn = ((CmsSession) element).getUserDn();
+                               return userDn.getRdn(userDn.size() - 1).getValue().toString();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return ((CmsSession) element).getUserDn().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // UUID
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("UUID");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return ((CmsSession) element).getUuid().toString();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return getText(element);
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Local ID
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("Local ID");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return ((CmsSession) element).getLocalId();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return getText(element);
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               viewer.setInput(bc);
+
+       }
+
+       @Focus
+       public void setFocus() {
+               if (viewer != null)
+                       viewer.getControl().setFocus();
+       }
+
+       /** Content provider managing the array of bundles */
+       private static class CmsSessionContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = -8533792785725875977L;
+
+               public Object[] getElements(Object inputElement) {
+                       if (inputElement instanceof BundleContext) {
+                               BundleContext bc = (BundleContext) inputElement;
+                               Collection<ServiceReference<CmsSession>> srs;
+                               try {
+                                       srs = bc.getServiceReferences(CmsSession.class, null);
+                               } catch (InvalidSyntaxException e) {
+                                       throw new CmsException("Cannot retrieve CMS sessions", e);
+                               }
+                               List<CmsSession> res = new ArrayList<>();
+                               for (ServiceReference<CmsSession> sr : srs) {
+                                       res.add(bc.getService(sr));
+                               }
+                               return res.toArray();
+                       }
+                       return null;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ModulesView.java
new file mode 100644 (file)
index 0000000..86f84df
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.monitoring;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+
+import org.argeo.eclipse.ui.TreeParent;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+/** The OSGi runtime from a module perspective. */
+public class ModulesView {
+       private final static BundleContext bc = FrameworkUtil.getBundle(ModulesView.class).getBundleContext();
+       private TreeViewer viewer;
+
+       @PostConstruct
+       public void createPartControl(Composite parent) {
+               viewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+               viewer.setContentProvider(new ModulesContentProvider());
+               viewer.setLabelProvider(new ModulesLabelProvider());
+               viewer.setInput(bc);
+       }
+
+       @Focus
+       public void setFocus() {
+               viewer.getTree().setFocus();
+       }
+
+       private class ModulesContentProvider implements ITreeContentProvider {
+               private static final long serialVersionUID = 3819934804640641721L;
+
+               public Object[] getElements(Object inputElement) {
+                       return getChildren(inputElement);
+               }
+
+               public Object[] getChildren(Object parentElement) {
+                       if (parentElement instanceof BundleContext) {
+                               BundleContext bundleContext = (BundleContext) parentElement;
+                               Bundle[] bundles = bundleContext.getBundles();
+
+                               List<BundleNode> modules = new ArrayList<BundleNode>();
+                               for (Bundle bundle : bundles) {
+                                       if (bundle.getState() == Bundle.ACTIVE)
+                                               modules.add(new BundleNode(bundle, true));
+                               }
+                               return modules.toArray();
+                       } else if (parentElement instanceof TreeParent) {
+                               return ((TreeParent) parentElement).getChildren();
+                       } else {
+                               return null;
+                       }
+               }
+
+               public Object getParent(Object element) {
+                       // TODO Auto-generated method stub
+                       return null;
+               }
+
+               public boolean hasChildren(Object element) {
+                       if (element instanceof TreeParent) {
+                               return ((TreeParent) element).hasChildren();
+                       }
+                       return false;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+
+       private class ModulesLabelProvider extends StateLabelProvider {
+               private static final long serialVersionUID = 5290046145534824722L;
+
+               @Override
+               public String getText(Object element) {
+                       if (element instanceof BundleNode)
+                               return element.toString() + " [" + ((BundleNode) element).getBundle().getBundleId() + "]";
+                       return element.toString();
+               }
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/OsgiConfigurationsView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/OsgiConfigurationsView.java
new file mode 100644 (file)
index 0000000..759b3e9
--- /dev/null
@@ -0,0 +1,163 @@
+package org.argeo.cms.e4.monitoring;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Dictionary;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+
+import org.argeo.cms.CmsException;
+import org.argeo.util.LangUtils;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.TreeViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+public class OsgiConfigurationsView {
+       private final static BundleContext bc = FrameworkUtil.getBundle(OsgiConfigurationsView.class).getBundleContext();
+
+       @PostConstruct
+       public void createPartControl(Composite parent) {
+               ConfigurationAdmin configurationAdmin = bc.getService(bc.getServiceReference(ConfigurationAdmin.class));
+
+               TreeViewer viewer = new TreeViewer(parent);
+               // viewer.getTree().setHeaderVisible(true);
+
+               TreeViewerColumn tvc = new TreeViewerColumn(viewer, SWT.NONE);
+               tvc.getColumn().setWidth(400);
+               tvc.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = 835407996597566763L;
+
+                       @Override
+                       public String getText(Object element) {
+                               if (element instanceof Configuration) {
+                                       return ((Configuration) element).getPid();
+                               } else if (element instanceof Prop) {
+                                       return ((Prop) element).key;
+                               }
+                               return super.getText(element);
+                       }
+
+                       @Override
+                       public Image getImage(Object element) {
+                               if (element instanceof Configuration)
+                                       return OsgiExplorerImages.CONFIGURATION;
+                               return null;
+                       }
+
+               });
+
+               tvc = new TreeViewerColumn(viewer, SWT.NONE);
+               tvc.getColumn().setWidth(400);
+               tvc.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = 6999659261190014687L;
+
+                       @Override
+                       public String getText(Object element) {
+                               if (element instanceof Configuration) {
+                                       // return ((Configuration) element).getFactoryPid();
+                                       return null;
+                               } else if (element instanceof Prop) {
+                                       return ((Prop) element).value.toString();
+                               }
+                               return super.getText(element);
+                       }
+               });
+
+               viewer.setContentProvider(new ConfigurationsContentProvider());
+               viewer.setInput(configurationAdmin);
+       }
+
+       static class ConfigurationsContentProvider implements ITreeContentProvider {
+               private static final long serialVersionUID = -4892768279440981042L;
+               private ConfigurationComparator configurationComparator = new ConfigurationComparator();
+
+               @Override
+               public void dispose() {
+               }
+
+               @Override
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+
+               @Override
+               public Object[] getElements(Object inputElement) {
+                       ConfigurationAdmin configurationAdmin = (ConfigurationAdmin) inputElement;
+                       try {
+                               Configuration[] configurations = configurationAdmin.listConfigurations(null);
+                               Arrays.sort(configurations, configurationComparator);
+                               return configurations;
+                       } catch (IOException | InvalidSyntaxException e) {
+                               throw new CmsException("Cannot list configurations", e);
+                       }
+               }
+
+               @Override
+               public Object[] getChildren(Object parentElement) {
+                       if (parentElement instanceof Configuration) {
+                               List<Prop> res = new ArrayList<>();
+                               Configuration configuration = (Configuration) parentElement;
+                               Dictionary<String, Object> props = configuration.getProperties();
+                               keys: for (String key : LangUtils.keys(props)) {
+                                       if (Constants.SERVICE_PID.equals(key))
+                                               continue keys;
+                                       if (ConfigurationAdmin.SERVICE_FACTORYPID.equals(key))
+                                               continue keys;
+                                       res.add(new Prop(configuration, key, props.get(key)));
+                               }
+                               return res.toArray(new Prop[res.size()]);
+                       }
+                       return null;
+               }
+
+               @Override
+               public Object getParent(Object element) {
+                       if (element instanceof Prop)
+                               return ((Prop) element).configuration;
+                       return null;
+               }
+
+               @Override
+               public boolean hasChildren(Object element) {
+                       if (element instanceof Configuration)
+                               return true;
+                       return false;
+               }
+
+       }
+
+       static class Prop {
+               final Configuration configuration;
+               final String key;
+               final Object value;
+
+               public Prop(Configuration configuration, String key, Object value) {
+                       this.configuration = configuration;
+                       this.key = key;
+                       this.value = value;
+               }
+
+       }
+
+       static class ConfigurationComparator implements Comparator<Configuration> {
+
+               @Override
+               public int compare(Configuration o1, Configuration o2) {
+                       return o1.getPid().compareTo(o2.getPid());
+               }
+
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/OsgiExplorerImages.java
new file mode 100644 (file)
index 0000000..35617e8
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.monitoring;
+
+import org.argeo.cms.ui.theme.CmsImages;
+import org.eclipse.swt.graphics.Image;
+
+/** Shared icons. */
+public class OsgiExplorerImages extends CmsImages {
+       public final static Image INSTALLED = createIcon("installed.gif");
+       public final static Image RESOLVED = createIcon("resolved.gif");
+       public final static Image STARTING = createIcon("starting.gif");
+       public final static Image ACTIVE = createIcon("active.gif");
+       public final static Image SERVICE_PUBLISHED = createIcon("service_published.gif");
+       public final static Image SERVICE_REFERENCED = createIcon("service_referenced.gif");
+       public final static Image CONFIGURATION = createIcon("node.gif");
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/ServiceReferenceNode.java
new file mode 100644 (file)
index 0000000..d9c45fe
--- /dev/null
@@ -0,0 +1,46 @@
+package org.argeo.cms.e4.monitoring;
+
+import org.argeo.eclipse.ui.TreeParent;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+
+/** A tree element representing a {@link ServiceReference} */
+@SuppressWarnings({ "rawtypes" })
+class ServiceReferenceNode extends TreeParent {
+       private final ServiceReference serviceReference;
+       private final boolean published;
+
+       public ServiceReferenceNode(ServiceReference serviceReference,
+                       boolean published) {
+               super(serviceReference.toString());
+               this.serviceReference = serviceReference;
+               this.published = published;
+
+               if (isPublished()) {
+                       Bundle[] usedBundles = serviceReference.getUsingBundles();
+                       if (usedBundles != null) {
+                               for (Bundle b : usedBundles) {
+                                       if (b != null)
+                                               addChild(new BundleNode(b));
+                               }
+                       }
+               } else {
+                       Bundle provider = serviceReference.getBundle();
+                       addChild(new BundleNode(provider));
+               }
+
+               for (String key : serviceReference.getPropertyKeys()) {
+                       addChild(new TreeParent(key + "="
+                                       + serviceReference.getProperty(key)));
+               }
+
+       }
+
+       public ServiceReference getServiceReference() {
+               return serviceReference;
+       }
+
+       public boolean isPublished() {
+               return published;
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/monitoring/StateLabelProvider.java
new file mode 100644 (file)
index 0000000..5cb5b65
--- /dev/null
@@ -0,0 +1,82 @@
+package org.argeo.cms.e4.monitoring;
+
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+
+/** Label provider showing the sate of bundles */
+class StateLabelProvider extends ColumnLabelProvider {
+       private static final long serialVersionUID = -7885583135316000733L;
+
+       @Override
+       public Image getImage(Object element) {
+               int state;
+               if (element instanceof Bundle)
+                       state = ((Bundle) element).getState();
+               else if (element instanceof BundleNode)
+                       state = ((BundleNode) element).getBundle().getState();
+               else if (element instanceof ServiceReferenceNode)
+                       if (((ServiceReferenceNode) element).isPublished())
+                               return OsgiExplorerImages.SERVICE_PUBLISHED;
+                       else
+                               return OsgiExplorerImages.SERVICE_REFERENCED;
+               else
+                       return null;
+
+               switch (state) {
+               case Bundle.UNINSTALLED:
+                       return OsgiExplorerImages.INSTALLED;
+               case Bundle.INSTALLED:
+                       return OsgiExplorerImages.INSTALLED;
+               case Bundle.RESOLVED:
+                       return OsgiExplorerImages.RESOLVED;
+               case Bundle.STARTING:
+                       return OsgiExplorerImages.STARTING;
+               case Bundle.STOPPING:
+                       return OsgiExplorerImages.STARTING;
+               case Bundle.ACTIVE:
+                       return OsgiExplorerImages.ACTIVE;
+               default:
+                       return null;
+               }
+       }
+
+       @Override
+       public String getText(Object element) {
+               return null;
+       }
+
+       @Override
+       public String getToolTipText(Object element) {
+               Bundle bundle = (Bundle) element;
+               Integer state = bundle.getState();
+               switch (state) {
+               case Bundle.UNINSTALLED:
+                       return "UNINSTALLED";
+               case Bundle.INSTALLED:
+                       return "INSTALLED";
+               case Bundle.RESOLVED:
+                       return "RESOLVED";
+               case Bundle.STARTING:
+                       String activationPolicy = bundle.getHeaders()
+                                       .get(Constants.BUNDLE_ACTIVATIONPOLICY).toString();
+
+                       // .get("Bundle-ActivationPolicy").toString();
+                       // FIXME constant triggers the compilation failure
+                       if (activationPolicy != null
+                                       && activationPolicy.equals(Constants.ACTIVATION_LAZY))
+                               // && activationPolicy.equals("lazy"))
+                               // FIXME constant triggers the compilation failure
+                               // && activationPolicy.equals(Constants.ACTIVATION_LAZY))
+                               return "<<LAZY>>";
+                       return "STARTING";
+               case Bundle.STOPPING:
+                       return "STOPPING";
+               case Bundle.ACTIVE:
+                       return "ACTIVE";
+               default:
+                       return null;
+               }
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/parts/CmsDocBookEditor.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/parts/CmsDocBookEditor.java
new file mode 100644 (file)
index 0000000..b39feab
--- /dev/null
@@ -0,0 +1,81 @@
+package org.argeo.cms.e4.parts;
+
+import java.util.Observable;
+import java.util.Observer;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.text.DocumentTextEditor;
+import org.argeo.cms.viewers.JcrVersionCmsEditable;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.docbook.DocBookTypes;
+import org.eclipse.e4.ui.di.Persist;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+
+public class CmsDocBookEditor implements Observer {
+       @Inject
+       Repository repository;
+
+       @Inject
+       private MPart mpart;
+
+       Session session;
+       JcrVersionCmsEditable cmsEditable;
+
+       @PostConstruct
+       public void createUI(Composite parent) {
+               try {
+                       parent.setLayout(new GridLayout());
+                       session = repository.login();
+                       JcrUtils.loginOrCreateWorkspace(repository, "demo");
+                       Node textNode = JcrUtils.getOrAdd(session.getRootNode(), "article", DocBookTypes.ARTICLE);
+                       if (textNode.isCheckedOut())
+                               textNode.addMixin(NodeType.MIX_TITLE);
+                       cmsEditable = new JcrVersionCmsEditable(textNode);
+                       if (session.hasPendingChanges())
+                               session.save();
+                       cmsEditable.addObserver(this);
+                       DocumentTextEditor textEditor = new DocumentTextEditor(parent, SWT.NONE, textNode, cmsEditable);
+                       mpart.setDirty(isDirty());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot create text editor", e);
+               }
+       }
+
+       @PreDestroy
+       public void dispose() {
+               JcrUtils.logoutQuietly(session);
+       }
+
+       @Persist
+       public void save() {
+               try {
+                       session.save();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot save", e);
+               }
+               cmsEditable.stopEditing();
+       }
+
+       @Override
+       public void update(Observable o, Object arg) {
+               // CmsEditable cmsEditable = (CmsEditable) o;
+               mpart.setDirty(isDirty());
+       }
+
+       boolean isDirty() {
+               return cmsEditable.isEditing();
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/parts/CmsTextEditor.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/parts/CmsTextEditor.java
new file mode 100644 (file)
index 0000000..4021ead
--- /dev/null
@@ -0,0 +1,73 @@
+package org.argeo.cms.e4.parts;
+
+import java.util.Observable;
+import java.util.Observer;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.CmsTypes;
+import org.argeo.cms.text.StandardTextEditor;
+import org.argeo.cms.viewers.JcrVersionCmsEditable;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.e4.ui.di.Persist;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+
+public class CmsTextEditor implements Observer {
+       @Inject
+       Repository repository;
+
+       @Inject
+       private MPart mpart;
+
+       Session session;
+       JcrVersionCmsEditable cmsEditable;
+
+       @PostConstruct
+       public void createUI(Composite parent) {
+               try {
+                       parent.setLayout(new GridLayout());
+                       session = repository.login();
+                       JcrUtils.loginOrCreateWorkspace(repository, "demo");
+                       Node textNode = JcrUtils.getOrAdd(session.getRootNode(), "text", CmsTypes.CMS_TEXT);
+                       cmsEditable = new JcrVersionCmsEditable(textNode);
+                       if (session.hasPendingChanges())
+                               session.save();
+                       cmsEditable.addObserver(this);
+                       StandardTextEditor textEditor = new StandardTextEditor(parent, SWT.NONE, textNode, cmsEditable);
+                       mpart.setDirty(cmsEditable.isEditing());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot create text editor", e);
+               }
+       }
+
+       @PreDestroy
+       public void dispose() {
+               JcrUtils.logoutQuietly(session);
+       }
+
+       @Persist
+       public void save() {
+               cmsEditable.stopEditing();
+       }
+
+       @Override
+       public void update(Observable o, Object arg) {
+               // CmsEditable cmsEditable = (CmsEditable) o;
+               mpart.setDirty(isDirty());
+       }
+
+       boolean isDirty() {
+               return cmsEditable.isEditing();
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/sys/handlers/ShowDesktop.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/sys/handlers/ShowDesktop.java
new file mode 100644 (file)
index 0000000..769c167
--- /dev/null
@@ -0,0 +1,9 @@
+package org.argeo.cms.e4.sys.handlers;
+
+import org.eclipse.e4.core.di.annotations.Execute;
+
+public class ShowDesktop {
+       @Execute
+       public void execute() {
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/sys/handlers/Shutdown.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/sys/handlers/Shutdown.java
new file mode 100644 (file)
index 0000000..a5abe24
--- /dev/null
@@ -0,0 +1,9 @@
+package org.argeo.cms.e4.sys.handlers;
+
+import org.eclipse.e4.core.di.annotations.Execute;
+
+public class Shutdown {
+       @Execute
+       public void execute() {
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/AbstractRoleEditor.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/AbstractRoleEditor.java
new file mode 100644 (file)
index 0000000..518be8c
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.users;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+
+import org.argeo.cms.ui.eclipse.forms.AbstractFormPart;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.argeo.cms.ui.eclipse.forms.ManagedForm;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.naming.LdapAttrs;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.e4.ui.di.Persist;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Editor for a user, might be a user or a group. */
+public abstract class AbstractRoleEditor {
+
+       // public final static String USER_EDITOR_ID = WorkbenchUiPlugin.PLUGIN_ID +
+       // ".userEditor";
+       // public final static String GROUP_EDITOR_ID = WorkbenchUiPlugin.PLUGIN_ID +
+       // ".groupEditor";
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       protected UserAdminWrapper userAdminWrapper;
+
+       @Inject
+       private MPart mPart;
+
+       // @Inject
+       // Composite parent;
+
+       private UserAdmin userAdmin;
+
+       // Context
+       private User user;
+       private String username;
+
+       private NameChangeListener listener;
+
+       private ManagedForm managedForm;
+
+       // public void init(IEditorSite site, IEditorInput input) throws
+       // PartInitException {
+       @PostConstruct
+       public void init(Composite parent) {
+               this.userAdmin = userAdminWrapper.getUserAdmin();
+               username = mPart.getPersistedState().get(LdapAttrs.uid.name());
+               user = (User) userAdmin.getRole(username);
+
+               listener = new NameChangeListener(Display.getCurrent());
+               userAdminWrapper.addListener(listener);
+               updateEditorTitle(null);
+
+               managedForm = new ManagedForm(parent) {
+
+                       @Override
+                       public void staleStateChanged() {
+                               refresh();
+                       }
+               };
+               ScrolledComposite scrolled = managedForm.getForm();
+               Composite body = new Composite(scrolled, SWT.NONE);
+               scrolled.setContent(body);
+               createUi(body);
+               managedForm.refresh();
+       }
+
+       abstract void createUi(Composite parent);
+
+       /**
+        * returns the list of all authorizations for the given user or of the current
+        * displayed user if parameter is null
+        */
+       protected List<User> getFlatGroups(User aUser) {
+               Authorization currAuth;
+               if (aUser == null)
+                       currAuth = userAdmin.getAuthorization(this.user);
+               else
+                       currAuth = userAdmin.getAuthorization(aUser);
+
+               String[] roles = currAuth.getRoles();
+
+               List<User> groups = new ArrayList<User>();
+               for (String roleStr : roles) {
+                       User currRole = (User) userAdmin.getRole(roleStr);
+                       if (currRole != null && !groups.contains(currRole))
+                               groups.add(currRole);
+               }
+               return groups;
+       }
+
+       protected IManagedForm getManagedForm() {
+               return managedForm;
+       }
+
+       /** Exposes the user (or group) that is displayed by the current editor */
+       protected User getDisplayedUser() {
+               return user;
+       }
+
+       private void setDisplayedUser(User user) {
+               this.user = user;
+       }
+
+       void updateEditorTitle(String title) {
+               if (title == null) {
+                       String commonName = UserAdminUtils.getProperty(user, LdapAttrs.cn.name());
+                       title = "".equals(commonName) ? user.getName() : commonName;
+               }
+               setPartName(title);
+       }
+
+       protected void setPartName(String name) {
+               mPart.setLabel(name);
+       }
+
+       // protected void addPages() {
+       // try {
+       // if (user.getType() == Role.GROUP)
+       // addPage(new GroupMainPage(this, userAdminWrapper, repository, nodeInstance));
+       // else
+       // addPage(new UserMainPage(this, userAdminWrapper));
+       // } catch (Exception e) {
+       // throw new CmsException("Cannot add pages", e);
+       // }
+       // }
+
+       @Persist
+       public void doSave(IProgressMonitor monitor) {
+               userAdminWrapper.beginTransactionIfNeeded();
+               commitPages(true);
+               userAdminWrapper.commitOrNotifyTransactionStateChange();
+               // firePropertyChange(PROP_DIRTY);
+               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, user));
+       }
+
+       protected void commitPages(boolean b) {
+               managedForm.commit(b);
+       }
+
+       @PreDestroy
+       public void dispose() {
+               userAdminWrapper.removeListener(listener);
+               managedForm.dispose();
+       }
+
+       // CONTROLERS FOR THIS EDITOR AND ITS PAGES
+
+       class NameChangeListener extends UiUserAdminListener {
+               public NameChangeListener(Display display) {
+                       super(display);
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       Role changedRole = event.getRole();
+                       if (changedRole == null || changedRole.equals(user)) {
+                               updateEditorTitle(null);
+                               User reloadedUser = (User) userAdminWrapper.getUserAdmin().getRole(user.getName());
+                               setDisplayedUser(reloadedUser);
+                       }
+               }
+       }
+
+       class MainInfoListener extends UiUserAdminListener {
+               private final AbstractFormPart part;
+
+               public MainInfoListener(Display display, AbstractFormPart part) {
+                       super(display);
+                       this.part = part;
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       // Rollback
+                       if (event.getRole() == null)
+                               part.markStale();
+               }
+       }
+
+       class GroupChangeListener extends UiUserAdminListener {
+               private final AbstractFormPart part;
+
+               public GroupChangeListener(Display display, AbstractFormPart part) {
+                       super(display);
+                       this.part = part;
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       // always mark as stale
+                       part.markStale();
+               }
+       }
+
+       /** Registers a listener that will notify this part */
+       class FormPartML implements ModifyListener {
+               private static final long serialVersionUID = 6299808129505381333L;
+               private AbstractFormPart formPart;
+
+               public FormPartML(AbstractFormPart generalPart) {
+                       this.formPart = generalPart;
+               }
+
+               public void modifyText(ModifyEvent e) {
+                       // Discard event when the control does not have the focus, typically
+                       // to avoid all editors being marked as dirty during a Rollback
+                       if (((Control) e.widget).isFocusControl())
+                               formPart.markDirty();
+               }
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+
+       /** Creates label and multiline text. */
+       Text createLMT(Composite parent, String label, String value) {
+               Label lbl = new Label(parent, SWT.NONE);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               Text text = new Text(parent, SWT.NONE);
+               text.setText(value);
+               text.setLayoutData(new GridData(SWT.LEAD, SWT.FILL, true, true));
+               return text;
+       }
+
+       /** Creates label and password. */
+       Text createLP(Composite parent, String label, String value) {
+               Label lbl = new Label(parent, SWT.NONE);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               Text text = new Text(parent, SWT.PASSWORD | SWT.BORDER);
+               text.setText(value);
+               text.setLayoutData(new GridData(SWT.LEAD, SWT.FILL, true, false));
+               return text;
+       }
+
+       /** Creates label and text. */
+       Text createLT(Composite parent, String label, String value) {
+               Label lbl = new Label(parent, SWT.NONE);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+               Text text = new Text(parent, SWT.BORDER);
+               text.setText(value);
+               text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               // CmsUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+               return text;
+       }
+
+       Text createReadOnlyLT(Composite parent, String label, String value) {
+               Label lbl = new Label(parent, SWT.NONE);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+               Text text = new Text(parent, SWT.NONE);
+               text.setText(value);
+               text.setLayoutData(new GridData(SWT.LEAD, SWT.FILL, true, false));
+               text.setEditable(false);
+               // CmsUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+               return text;
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/CmsWorkbenchStyles.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/CmsWorkbenchStyles.java
new file mode 100644 (file)
index 0000000..07df312
--- /dev/null
@@ -0,0 +1,8 @@
+package org.argeo.cms.e4.users;
+
+/** Centralize the declaration of Workbench specific CSS Styles */
+interface CmsWorkbenchStyles {
+
+       // Specific People layouting
+       String WORKBENCH_FORM_TEXT = "workbench_form_text";
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/GroupEditor.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/GroupEditor.java
new file mode 100644 (file)
index 0000000..a388794
--- /dev/null
@@ -0,0 +1,587 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.users;
+
+import static org.argeo.cms.util.UserAdminUtils.setProperty;
+import static org.argeo.naming.LdapAttrs.businessCategory;
+import static org.argeo.naming.LdapAttrs.description;
+import static org.argeo.node.NodeInstance.WORKGROUP;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.transaction.UserTransaction;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.e4.users.providers.CommonNameLP;
+import org.argeo.cms.e4.users.providers.MailLP;
+import org.argeo.cms.e4.users.providers.RoleIconLP;
+import org.argeo.cms.e4.users.providers.UserFilter;
+import org.argeo.cms.ui.eclipse.forms.AbstractFormPart;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeInstance;
+import org.argeo.node.NodeUtils;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+//import org.eclipse.ui.forms.AbstractFormPart;
+//import org.eclipse.ui.forms.IManagedForm;
+//import org.eclipse.ui.forms.SectionPart;
+//import org.eclipse.ui.forms.editor.FormEditor;
+//import org.eclipse.ui.forms.editor.FormPage;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ScrolledForm;
+//import org.eclipse.ui.forms.widgets.Section;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Display/edit main properties of a given group */
+public class GroupEditor extends AbstractRoleEditor implements ArgeoNames {
+       // final static String ID = "GroupEditor.mainPage";
+
+       @Inject
+       private EPartService partService;
+
+       // private final UserEditor editor;
+       @Inject
+       private Repository repository;
+       @Inject
+       private NodeInstance nodeInstance;
+       // private final UserAdminWrapper userAdminWrapper;
+       private Session session;
+
+       // public GroupMainPage(FormEditor editor, UserAdminWrapper userAdminWrapper,
+       // Repository repository,
+       // NodeInstance nodeInstance) {
+       // super(editor, ID, "Main");
+       // try {
+       // session = repository.login();
+       // } catch (RepositoryException e) {
+       // throw new CmsException("Cannot retrieve session of in MainGroupPage
+       // constructor", e);
+       // }
+       // this.editor = (UserEditor) editor;
+       // this.userAdminWrapper = userAdminWrapper;
+       // this.nodeInstance = nodeInstance;
+       // }
+
+       // protected void createFormContent(final IManagedForm mf) {
+       // ScrolledForm form = mf.getForm();
+       // Composite body = form.getBody();
+       // GridLayout mainLayout = new GridLayout();
+       // body.setLayout(mainLayout);
+       // Group group = (Group) editor.getDisplayedUser();
+       // appendOverviewPart(body, group);
+       // appendMembersPart(body, group);
+       // }
+
+       @Override
+       protected void createUi(Composite parent) {
+               try {
+                       session = repository.login();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot retrieve session", e);
+               }
+               // ScrolledForm form = mf.getForm();
+               // Composite body = form.getBody();
+               // Composite body = new Composite(parent, SWT.NONE);
+               Composite body = parent;
+               GridLayout mainLayout = new GridLayout();
+               body.setLayout(mainLayout);
+               Group group = (Group) getDisplayedUser();
+               appendOverviewPart(body, group);
+               appendMembersPart(body, group);
+       }
+
+       @PreDestroy
+       public void dispose() {
+               JcrUtils.logoutQuietly(session);
+               super.dispose();
+       }
+
+       /** Creates the general section */
+       protected void appendOverviewPart(final Composite parent, final Group group) {
+               Composite body = new Composite(parent, SWT.NONE);
+               // GridLayout layout = new GridLayout(5, false);
+               GridLayout layout = new GridLayout(2, false);
+               body.setLayout(layout);
+               body.setLayoutData(CmsUtils.fillWidth());
+
+               String cn = UserAdminUtils.getProperty(group, LdapAttrs.cn.name());
+               createReadOnlyLT(body, "Name", cn);
+               createReadOnlyLT(body, "DN", group.getName());
+               createReadOnlyLT(body, "Domain", UserAdminUtils.getDomainName(group));
+
+               // Description
+               Label descLbl = new Label(body, SWT.LEAD);
+               descLbl.setFont(EclipseUiUtils.getBoldFont(body));
+               descLbl.setText("Description");
+               descLbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, true, false, 2, 1));
+               final Text descTxt = new Text(body, SWT.LEAD | SWT.MULTI | SWT.WRAP | SWT.BORDER);
+               GridData gd = EclipseUiUtils.fillWidth();
+               gd.heightHint = 50;
+               gd.horizontalSpan = 2;
+               descTxt.setLayoutData(gd);
+
+               // Mark as workgroup
+               Link markAsWorkgroupLk = new Link(body, SWT.NONE);
+               markAsWorkgroupLk.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 2, 1));
+
+               // create form part (controller)
+               final AbstractFormPart part = new AbstractFormPart() {
+
+                       private MainInfoListener listener;
+
+                       @Override
+                       public void initialize(IManagedForm form) {
+                               super.initialize(form);
+                               listener = new MainInfoListener(parent.getDisplay(), this);
+                               userAdminWrapper.addListener(listener);
+                       }
+
+                       @Override
+                       public void dispose() {
+                               userAdminWrapper.removeListener(listener);
+                               super.dispose();
+                       }
+
+                       public void commit(boolean onSave) {
+                               // group.getProperties().put(LdapAttrs.description.name(), descTxt.getText());
+                               setProperty(group, description, descTxt.getText());
+                               super.commit(onSave);
+                       }
+
+                       @Override
+                       public void refresh() {
+                               // dnTxt.setText(group.getName());
+                               // cnTxt.setText(UserAdminUtils.getProperty(group, LdapAttrs.cn.name()));
+                               descTxt.setText(UserAdminUtils.getProperty(group, LdapAttrs.description.name()));
+                               Node workgroupHome = NodeUtils.getGroupHome(session, cn);
+                               if (workgroupHome == null)
+                                       markAsWorkgroupLk.setText("<a>Mark as workgroup</a>");
+                               else
+                                       markAsWorkgroupLk.setText("Configured as workgroup");
+                               parent.layout(true, true);
+                               super.refresh();
+                       }
+               };
+
+               markAsWorkgroupLk.addSelectionListener(new SelectionAdapter() {
+                       private static final long serialVersionUID = -6439340898096365078L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+
+                               boolean confirmed = MessageDialog.openConfirm(parent.getShell(), "Mark as workgroup",
+                                               "Are you sure you want to mark " + cn + " as being a workgroup? ");
+                               if (confirmed) {
+                                       Node workgroupHome = NodeUtils.getGroupHome(session, cn);
+                                       if (workgroupHome != null)
+                                               return; // already marked as workgroup, do nothing
+                                       else
+                                               try {
+                                                       // improve transaction management
+                                                       userAdminWrapper.beginTransactionIfNeeded();
+                                                       nodeInstance.createWorkgroup(new LdapName(group.getName()));
+                                                       setProperty(group, businessCategory, WORKGROUP);
+                                                       userAdminWrapper.commitOrNotifyTransactionStateChange();
+                                                       userAdminWrapper
+                                                                       .notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+                                                       part.refresh();
+                                               } catch (InvalidNameException e1) {
+                                                       throw new CmsException("Cannot create Workgroup for " + group.toString(), e1);
+                                               }
+
+                               }
+                       }
+               });
+
+               ModifyListener defaultListener = new FormPartML(part);
+               descTxt.addModifyListener(defaultListener);
+               getManagedForm().addPart(part);
+       }
+
+       /** Filtered table with members. Has drag and drop ability */
+       protected void appendMembersPart(Composite parent, Group group) {
+               // Section section = tk.createSection(parent, Section.TITLE_BAR);
+               // section.setText("Members");
+               // section.setLayoutData(EclipseUiUtils.fillAll());
+
+               Composite body = new Composite(parent, SWT.BORDER);
+               body.setLayout(new GridLayout());
+               // section.setClient(body);
+               body.setLayoutData(EclipseUiUtils.fillAll());
+
+               // Define the displayed columns
+               List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+               columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24));
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+               columnDefs.add(new ColumnDefinition(new MailLP(), "Mail", 150));
+               // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name",
+               // 240));
+
+               // Create and configure the table
+               LdifUsersTable userViewerCmp = new MyUserTableViewer(body, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL,
+                               userAdminWrapper.getUserAdmin());
+
+               userViewerCmp.setColumnDefinitions(columnDefs);
+               userViewerCmp.populate(true, false);
+               userViewerCmp.setLayoutData(EclipseUiUtils.fillAll());
+
+               // Controllers
+               TableViewer userViewer = userViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService));
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               userViewer.addDropSupport(operations, tt,
+                               new GroupDropListener(userAdminWrapper, userViewerCmp, (Group) getDisplayedUser()));
+
+               AbstractFormPart part = new GroupMembersPart(userViewerCmp);
+               getManagedForm().addPart(part);
+
+               // remove button
+               // addRemoveAbility(toolBarManager, userViewerCmp.getTableViewer(), group);
+               Action action = new RemoveMembershipAction(userViewer, group, "Remove selected items from this group",
+                               SecurityAdminImages.ICON_REMOVE_DESC);
+
+               ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+               ToolBar toolBar = toolBarManager.createControl(body);
+               toolBar.setLayoutData(CmsUtils.fillWidth());
+
+               toolBarManager.add(action);
+               toolBarManager.update(true);
+
+       }
+
+       // private LdifUsersTable createMemberPart(Composite parent, Group group) {
+       //
+       // // Define the displayed columns
+       // List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+       // columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24));
+       // columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+       // columnDefs.add(new ColumnDefinition(new MailLP(), "Mail", 150));
+       // // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished
+       // Name",
+       // // 240));
+       //
+       // // Create and configure the table
+       // LdifUsersTable userViewerCmp = new MyUserTableViewer(parent, SWT.MULTI |
+       // SWT.H_SCROLL | SWT.V_SCROLL,
+       // userAdminWrapper.getUserAdmin());
+       //
+       // userViewerCmp.setColumnDefinitions(columnDefs);
+       // userViewerCmp.populate(true, false);
+       // userViewerCmp.setLayoutData(EclipseUiUtils.fillAll());
+       //
+       // // Controllers
+       // TableViewer userViewer = userViewerCmp.getTableViewer();
+       // userViewer.addDoubleClickListener(new
+       // UserTableDefaultDClickListener(partService));
+       // int operations = DND.DROP_COPY | DND.DROP_MOVE;
+       // Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+       // userViewer.addDropSupport(operations, tt,
+       // new GroupDropListener(userAdminWrapper, userViewerCmp, (Group)
+       // getDisplayedUser()));
+       //
+       // // userViewerCmp.refresh();
+       // return userViewerCmp;
+       // }
+
+       // Local viewers
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private final UserFilter userFilter;
+
+               public MyUserTableViewer(Composite parent, int style, UserAdmin userAdmin) {
+                       super(parent, style, true);
+                       userFilter = new UserFilter();
+
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       // reload user and set it in the editor
+                       Group group = (Group) getDisplayedUser();
+                       Role[] roles = group.getMembers();
+                       List<User> users = new ArrayList<User>();
+                       userFilter.setSearchText(filter);
+                       // userFilter.setShowSystemRole(true);
+                       for (Role role : roles)
+                               // if (role.getType() == Role.GROUP)
+                               if (userFilter.select(null, null, role))
+                                       users.add((User) role);
+                       return users;
+               }
+       }
+
+       // private void addRemoveAbility(ToolBarManager toolBarManager, TableViewer
+       // userViewer, Group group) {
+       // // Section section = sectionPart.getSection();
+       // // ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+       // // ToolBar toolbar = toolBarManager.createControl(parent);
+       // // ToolBar toolbar = toolBarManager.getControl();
+       // // final Cursor handCursor = new Cursor(toolbar.getDisplay(),
+       // SWT.CURSOR_HAND);
+       // // toolbar.setCursor(handCursor);
+       // // toolbar.addDisposeListener(new DisposeListener() {
+       // // private static final long serialVersionUID = 3882131405820522925L;
+       // //
+       // // public void widgetDisposed(DisposeEvent e) {
+       // // if ((handCursor != null) && (handCursor.isDisposed() == false)) {
+       // // handCursor.dispose();
+       // // }
+       // // }
+       // // });
+       //
+       // Action action = new RemoveMembershipAction(userViewer, group, "Remove
+       // selected items from this group",
+       // SecurityAdminImages.ICON_REMOVE_DESC);
+       // toolBarManager.add(action);
+       // toolBarManager.update(true);
+       // // section.setTextClient(toolbar);
+       // }
+
+       private class RemoveMembershipAction extends Action {
+               private static final long serialVersionUID = -1337713097184522588L;
+
+               private final TableViewer userViewer;
+               private final Group group;
+
+               RemoveMembershipAction(TableViewer userViewer, Group group, String name, ImageDescriptor img) {
+                       super(name, img);
+                       this.userViewer = userViewer;
+                       this.group = group;
+               }
+
+               @Override
+               public void run() {
+                       ISelection selection = userViewer.getSelection();
+                       if (selection.isEmpty())
+                               return;
+
+                       @SuppressWarnings("unchecked")
+                       Iterator<User> it = ((IStructuredSelection) selection).iterator();
+                       List<User> users = new ArrayList<User>();
+                       while (it.hasNext()) {
+                               User currUser = it.next();
+                               users.add(currUser);
+                       }
+
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       for (User user : users) {
+                               group.removeMember(user);
+                       }
+                       userAdminWrapper.commitOrNotifyTransactionStateChange();
+                       userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+               }
+       }
+
+       // LOCAL CONTROLLERS
+       private class GroupMembersPart extends AbstractFormPart {
+               private final LdifUsersTable userViewer;
+               // private final Group group;
+
+               private GroupChangeListener listener;
+
+               public GroupMembersPart(LdifUsersTable userViewer) {
+                       // super(section);
+                       this.userViewer = userViewer;
+                       // this.group = group;
+               }
+
+               @Override
+               public void initialize(IManagedForm form) {
+                       super.initialize(form);
+                       listener = new GroupChangeListener(userViewer.getDisplay(), GroupMembersPart.this);
+                       userAdminWrapper.addListener(listener);
+               }
+
+               @Override
+               public void dispose() {
+                       userAdminWrapper.removeListener(listener);
+                       super.dispose();
+               }
+
+               @Override
+               public void refresh() {
+                       userViewer.refresh();
+                       super.refresh();
+               }
+       }
+
+       /**
+        * Defines this table as being a potential target to add group membership
+        * (roles) to this group
+        */
+       private class GroupDropListener extends ViewerDropAdapter {
+               private static final long serialVersionUID = 2893468717831451621L;
+
+               private final UserAdminWrapper userAdminWrapper;
+               // private final LdifUsersTable myUserViewerCmp;
+               private final Group myGroup;
+
+               public GroupDropListener(UserAdminWrapper userAdminWrapper, LdifUsersTable userTableViewerCmp, Group group) {
+                       super(userTableViewerCmp.getTableViewer());
+                       this.userAdminWrapper = userAdminWrapper;
+                       this.myGroup = group;
+                       // this.myUserViewerCmp = userTableViewerCmp;
+               }
+
+               @Override
+               public boolean validateDrop(Object target, int operation, TransferData transferType) {
+                       // Target is always OK in a list only view
+                       // TODO check if not a string
+                       boolean validDrop = true;
+                       return validDrop;
+               }
+
+               @Override
+               public void drop(DropTargetEvent event) {
+                       // TODO Is there an opportunity to perform the check before?
+                       String newUserName = (String) event.data;
+                       UserAdmin myUserAdmin = userAdminWrapper.getUserAdmin();
+                       Role role = myUserAdmin.getRole(newUserName);
+                       if (role.getType() == Role.GROUP) {
+                               Group newGroup = (Group) role;
+                               Shell shell = getViewer().getControl().getShell();
+                               // Sanity checks
+                               if (myGroup == newGroup) { // Equality
+                                       MessageDialog.openError(shell, "Forbidden addition ", "A group cannot be a member of itself.");
+                                       return;
+                               }
+
+                               // Cycle
+                               String myName = myGroup.getName();
+                               List<User> myMemberships = getFlatGroups(myGroup);
+                               if (myMemberships.contains(newGroup)) {
+                                       MessageDialog.openError(shell, "Forbidden addition: cycle",
+                                                       "Cannot add " + newUserName + " to group " + myName + ". This would create a cycle");
+                                       return;
+                               }
+
+                               // Already member
+                               List<User> newGroupMemberships = getFlatGroups(newGroup);
+                               if (newGroupMemberships.contains(myGroup)) {
+                                       MessageDialog.openError(shell, "Forbidden addition",
+                                                       "Cannot add " + newUserName + " to group " + myName + ", this membership already exists");
+                                       return;
+                               }
+                               userAdminWrapper.beginTransactionIfNeeded();
+                               myGroup.addMember(newGroup);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, myGroup));
+                       } else if (role.getType() == Role.USER) {
+                               // TODO check if the group is already member of this group
+                               UserTransaction transaction = userAdminWrapper.beginTransactionIfNeeded();
+                               User user = (User) role;
+                               myGroup.addMember(user);
+                               if (UserAdminWrapper.COMMIT_ON_SAVE)
+                                       try {
+                                               transaction.commit();
+                                       } catch (Exception e) {
+                                               throw new CmsException("Cannot commit transaction " + "after user group membership update", e);
+                                       }
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, myGroup));
+                       }
+                       super.drop(event);
+               }
+
+               @Override
+               public boolean performDrop(Object data) {
+                       // myUserViewerCmp.refresh();
+                       return true;
+               }
+       }
+
+       // LOCAL HELPERS
+       // private Composite addSection(FormToolkit tk, Composite parent) {
+       // Section section = tk.createSection(parent, SWT.NO_FOCUS);
+       // section.setLayoutData(EclipseUiUtils.fillWidth());
+       // Composite body = tk.createComposite(section, SWT.WRAP);
+       // body.setLayoutData(EclipseUiUtils.fillAll());
+       // section.setClient(body);
+       // return body;
+       // }
+
+       /** Creates label and text. */
+       // private Text createLT(Composite parent, String label, String value) {
+       // FormToolkit toolkit = getManagedForm().getToolkit();
+       // Label lbl = toolkit.createLabel(parent, label);
+       // lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+       // lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+       // Text text = toolkit.createText(parent, value, SWT.BORDER);
+       // text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+       // CmsUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+       // return text;
+       // }
+       //
+       // Text createReadOnlyLT(Composite parent, String label, String value) {
+       // FormToolkit toolkit = getManagedForm().getToolkit();
+       // Label lbl = toolkit.createLabel(parent, label);
+       // lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+       // lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+       // Text text = toolkit.createText(parent, value, SWT.NONE);
+       // text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+       // text.setEditable(false);
+       // CmsUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+       // return text;
+       // }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/GroupsView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/GroupsView.java
new file mode 100644 (file)
index 0000000..53937c9
--- /dev/null
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.users;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.e4.users.providers.CommonNameLP;
+import org.argeo.cms.e4.users.providers.DomainNameLP;
+import org.argeo.cms.e4.users.providers.RoleIconLP;
+import org.argeo.cms.e4.users.providers.UserDragListener;
+//import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+//import org.argeo.cms.ui.workbench.internal.useradmin.UiUserAdminListener;
+//import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+//import org.argeo.cms.ui.workbench.internal.useradmin.providers.CommonNameLP;
+//import org.argeo.cms.ui.workbench.internal.useradmin.providers.DomainNameLP;
+//import org.argeo.cms.ui.workbench.internal.useradmin.providers.RoleIconLP;
+//import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserDragListener;
+//import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserTableDefaultDClickListener;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.LdapObjs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+//import org.eclipse.ui.part.ViewPart;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** List all groups with filter */
+public class GroupsView implements ArgeoNames {
+       private final static Log log = LogFactory.getLog(GroupsView.class);
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".groupsView";
+
+       @Inject
+       private EPartService partService;
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+
+       // UI Objects
+       private LdifUsersTable groupTableViewerCmp;
+       private TableViewer userViewer;
+       private List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+
+       private UserAdminListener listener;
+
+       @PostConstruct
+       public void createPartControl(Composite parent, ESelectionService selectionService) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+               // boolean isAdmin = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+
+               // Define the displayed columns
+               columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 19));
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+               columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 100));
+               // Only show technical DN to admin
+               // if (isAdmin)
+               // columnDefs.add(new ColumnDefinition(new UserNameLP(),
+               // "Distinguished Name", 300));
+
+               // Create and configure the table
+               groupTableViewerCmp = new MyUserTableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+
+               groupTableViewerCmp.setColumnDefinitions(columnDefs);
+               // if (isAdmin)
+               // groupTableViewerCmp.populateWithStaticFilters(false, false);
+               // else
+               groupTableViewerCmp.populate(true, false);
+
+               groupTableViewerCmp.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               // Links
+               userViewer = groupTableViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService));
+               // getViewSite().setSelectionProvider(userViewer);
+               userViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+
+                       @Override
+                       public void selectionChanged(SelectionChangedEvent event) {
+                               IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+                               selectionService.setSelection(selection.toList());
+                       }
+               });
+
+               // Really?
+               groupTableViewerCmp.refresh();
+
+               // Drag and drop
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               userViewer.addDragSupport(operations, tt, new UserDragListener(userViewer));
+
+               // // Register a useradmin listener
+               // listener = new UserAdminListener() {
+               // @Override
+               // public void roleChanged(UserAdminEvent event) {
+               // if (userViewer != null && !userViewer.getTable().isDisposed())
+               // refresh();
+               // }
+               // };
+               // userAdminWrapper.addListener(listener);
+               // }
+
+               // Register a useradmin listener
+               listener = new MyUiUAListener(parent.getDisplay());
+               userAdminWrapper.addListener(listener);
+       }
+
+       private class MyUiUAListener extends UiUserAdminListener {
+               public MyUiUAListener(Display display) {
+                       super(display);
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       if (userViewer != null && !userViewer.getTable().isDisposed())
+                               refresh();
+               }
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private boolean showSystemRoles = true;
+
+               private final String[] knownProps = { LdapAttrs.uid.name(), LdapAttrs.cn.name(), LdapAttrs.DN };
+
+               public MyUserTableViewer(Composite parent, int style) {
+                       super(parent, style);
+                       showSystemRoles = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+               }
+
+               protected void populateStaticFilters(Composite staticFilterCmp) {
+                       staticFilterCmp.setLayout(new GridLayout());
+                       final Button showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK);
+                       showSystemRoleBtn.setText("Show system roles");
+                       showSystemRoles = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+                       showSystemRoleBtn.setSelection(showSystemRoles);
+
+                       showSystemRoleBtn.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = -7033424592697691676L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       showSystemRoles = showSystemRoleBtn.getSelection();
+                                       refresh();
+                               }
+
+                       });
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       Role[] roles;
+                       try {
+                               StringBuilder builder = new StringBuilder();
+                               StringBuilder tmpBuilder = new StringBuilder();
+                               if (EclipseUiUtils.notEmpty(filter))
+                                       for (String prop : knownProps) {
+                                               tmpBuilder.append("(");
+                                               tmpBuilder.append(prop);
+                                               tmpBuilder.append("=*");
+                                               tmpBuilder.append(filter);
+                                               tmpBuilder.append("*)");
+                                       }
+                               if (tmpBuilder.length() > 1) {
+                                       builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=")
+                                                       .append(LdapObjs.groupOfNames.name()).append(")");
+                                       if (!showSystemRoles)
+                                               builder.append("(!(").append(LdapAttrs.DN).append("=*").append(NodeConstants.ROLES_BASEDN)
+                                                               .append("))");
+                                       builder.append("(|");
+                                       builder.append(tmpBuilder.toString());
+                                       builder.append("))");
+                               } else {
+                                       if (!showSystemRoles)
+                                               builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=")
+                                                               .append(LdapObjs.groupOfNames.name()).append(")(!(").append(LdapAttrs.DN).append("=*")
+                                                               .append(NodeConstants.ROLES_BASEDN).append(")))");
+                                       else
+                                               builder.append("(").append(LdapAttrs.objectClass.name()).append("=")
+                                                               .append(LdapObjs.groupOfNames.name()).append(")");
+
+                               }
+                               roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString());
+                       } catch (InvalidSyntaxException e) {
+                               throw new CmsException("Unable to get roles with filter: " + filter, e);
+                       }
+                       List<User> users = new ArrayList<User>();
+                       for (Role role : roles)
+                               if (!users.contains(role))
+                                       users.add((User) role);
+                               else
+                                       log.warn("Duplicated role: " + role);
+
+                       return users;
+               }
+       }
+
+       public void refresh() {
+               groupTableViewerCmp.refresh();
+       }
+
+       @PreDestroy
+       public void dispose() {
+               userAdminWrapper.removeListener(listener);
+       }
+
+       @Focus
+       public void setFocus() {
+               groupTableViewerCmp.setFocus();
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/SecurityAdminImages.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/SecurityAdminImages.java
new file mode 100644 (file)
index 0000000..ca2db9d
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Argeo Connect - Data management and communications
+ * Copyright (C) 2012 Argeo GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>
+ *
+ * Additional permission under GNU GPL version 3 section 7
+ *
+ * If you modify this Program, or any covered work, by linking or combining it
+ * with software covered by the terms of the Eclipse Public License, the
+ * licensors of this Program grant you additional permission to convey the
+ * resulting work. Corresponding Source for a non-source form of such a
+ * combination shall include the source code for the parts of such software
+ * which are used as well as that of the covered work.
+ */
+package org.argeo.cms.e4.users;
+
+import org.argeo.cms.ui.theme.CmsImages;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+
+/** Shared icons that must be declared programmatically . */
+public class SecurityAdminImages extends CmsImages {
+       private final static String PREFIX = "icons/";
+
+       public final static ImageDescriptor ICON_REMOVE_DESC = createDesc(PREFIX + "delete.png");
+       public final static ImageDescriptor ICON_USER_DESC = createDesc(PREFIX + "person.png");
+
+       public final static Image ICON_USER = ICON_USER_DESC.createImage();
+       public final static Image ICON_GROUP = createImg(PREFIX + "group.png");
+       public final static Image ICON_WORKGROUP = createImg(PREFIX + "workgroup.png");
+       public final static Image ICON_ROLE = createImg(PREFIX + "role.gif");
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UiAdminUtils.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UiAdminUtils.java
new file mode 100644 (file)
index 0000000..a5fb610
--- /dev/null
@@ -0,0 +1,34 @@
+package org.argeo.cms.e4.users;
+
+import javax.transaction.UserTransaction;
+
+/** First effort to centralize back end methods used by the user admin UI */
+public class UiAdminUtils {
+       /*
+        * INTERNAL METHODS: Below methods are meant to stay here and are not part
+        * of a potential generic backend to manage the useradmin
+        */
+       /** Easily notify the ActiveWindow that the transaction had a state change */
+       public final static void notifyTransactionStateChange(
+                       UserTransaction userTransaction) {
+//             try {
+//                     IWorkbenchWindow aww = PlatformUI.getWorkbench()
+//                                     .getActiveWorkbenchWindow();
+//                     ISourceProviderService sourceProviderService = (ISourceProviderService) aww
+//                                     .getService(ISourceProviderService.class);
+//                     UserTransactionProvider esp = (UserTransactionProvider) sourceProviderService
+//                                     .getSourceProvider(UserTransactionProvider.TRANSACTION_STATE);
+//                     esp.fireTransactionStateChange();
+//             } catch (Exception e) {
+//                     throw new CmsException("Unable to begin transaction", e);
+//             }
+       }
+
+       /**
+        * Email addresses must match this regexp pattern ({@value #EMAIL_PATTERN}.
+        * Thanks to <a href=
+        * "http://www.mkyong.com/regular-expressions/how-to-validate-email-address-with-regular-expression/"
+        * >this tip</a>.
+        */
+       public final static String EMAIL_PATTERN = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UiUserAdminListener.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UiUserAdminListener.java
new file mode 100644 (file)
index 0000000..eb64aba
--- /dev/null
@@ -0,0 +1,27 @@
+package org.argeo.cms.e4.users;
+
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** Convenience class to insure the call to refresh is done in the UI thread */
+public abstract class UiUserAdminListener implements UserAdminListener {
+
+       private final Display display;
+
+       public UiUserAdminListener(Display display) {
+               this.display = display;
+       }
+
+       @Override
+       public void roleChanged(final UserAdminEvent event) {
+               display.asyncExec(new Runnable() {
+                       @Override
+                       public void run() {
+                               roleChangedToUiThread(event);
+                       }
+               });
+       }
+
+       public abstract void roleChangedToUiThread(UserAdminEvent event);
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserAdminWrapper.java
new file mode 100644 (file)
index 0000000..5eecaac
--- /dev/null
@@ -0,0 +1,132 @@
+package org.argeo.cms.e4.users;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.transaction.Status;
+import javax.transaction.UserTransaction;
+
+import org.argeo.cms.CmsException;
+import org.argeo.node.NodeConstants;
+import org.argeo.osgi.useradmin.UserAdminConf;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** Centralise interaction with the UserAdmin in this bundle */
+public class UserAdminWrapper {
+
+       private UserAdmin userAdmin;
+       // private ServiceReference<UserAdmin> userAdminServiceReference;
+       private Set<String> uris;
+       private UserTransaction userTransaction;
+
+       // First effort to simplify UX while managing users and groups
+       public final static boolean COMMIT_ON_SAVE = true;
+
+       // Registered listeners
+       List<UserAdminListener> listeners = new ArrayList<UserAdminListener>();
+
+       /**
+        * Starts a transaction if necessary. Should always been called together with
+        * {@link UserAdminWrapper#commitOrNotifyTransactionStateChange()} once the
+        * security model changes have been performed.
+        */
+       public UserTransaction beginTransactionIfNeeded() {
+               try {
+                       // UserTransaction userTransaction = getUserTransaction();
+                       if (userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION) {
+                               userTransaction.begin();
+                               // UiAdminUtils.notifyTransactionStateChange(userTransaction);
+                       }
+                       return userTransaction;
+               } catch (Exception e) {
+                       throw new CmsException("Unable to begin transaction", e);
+               }
+       }
+
+       /**
+        * Depending on the current application configuration, it will either commit the
+        * current transaction or throw a notification that the transaction state has
+        * changed (In the later case, it must be called from the UI thread).
+        */
+       public void commitOrNotifyTransactionStateChange() {
+               try {
+                       // UserTransaction userTransaction = getUserTransaction();
+                       if (userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION)
+                               return;
+
+                       if (UserAdminWrapper.COMMIT_ON_SAVE)
+                               userTransaction.commit();
+                       else
+                               UiAdminUtils.notifyTransactionStateChange(userTransaction);
+               } catch (Exception e) {
+                       throw new CmsException("Unable to clean transaction", e);
+               }
+       }
+
+       // TODO implement safer mechanism
+       public void addListener(UserAdminListener userAdminListener) {
+               if (!listeners.contains(userAdminListener))
+                       listeners.add(userAdminListener);
+       }
+
+       public void removeListener(UserAdminListener userAdminListener) {
+               if (listeners.contains(userAdminListener))
+                       listeners.remove(userAdminListener);
+       }
+
+       public void notifyListeners(UserAdminEvent event) {
+               for (UserAdminListener listener : listeners)
+                       listener.roleChanged(event);
+       }
+
+       public Map<String, String> getKnownBaseDns(boolean onlyWritable) {
+               Map<String, String> dns = new HashMap<String, String>();
+               for (String uri : uris) {
+                       if (!uri.startsWith("/"))
+                               continue;
+                       Dictionary<String, ?> props = UserAdminConf.uriAsProperties(uri);
+                       String readOnly = UserAdminConf.readOnly.getValue(props);
+                       String baseDn = UserAdminConf.baseDn.getValue(props);
+
+                       if (onlyWritable && "true".equals(readOnly))
+                               continue;
+                       if (baseDn.equalsIgnoreCase(NodeConstants.ROLES_BASEDN))
+                               continue;
+                       if (baseDn.equalsIgnoreCase(NodeConstants.TOKENS_BASEDN))
+                               continue;
+                       dns.put(baseDn, uri);
+               }
+               return dns;
+       }
+
+       public UserAdmin getUserAdmin() {
+               return userAdmin;
+       }
+
+       public UserTransaction getUserTransaction() {
+               return userTransaction;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdmin(UserAdmin userAdmin, Map<String, String> properties) {
+               this.userAdmin = userAdmin;
+               this.uris = Collections.unmodifiableSortedSet(new TreeSet<>(properties.keySet()));
+       }
+
+       public void setUserTransaction(UserTransaction userTransaction) {
+               this.userTransaction = userTransaction;
+       }
+
+       // public void setUserAdminServiceReference(
+       // ServiceReference<UserAdmin> userAdminServiceReference) {
+       // this.userAdminServiceReference = userAdminServiceReference;
+       // }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserBatchUpdateWizard.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserBatchUpdateWizard.java
new file mode 100644 (file)
index 0000000..6ce51b8
--- /dev/null
@@ -0,0 +1,625 @@
+package org.argeo.cms.e4.users;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.transaction.SystemException;
+import javax.transaction.UserTransaction;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.e4.users.providers.CommonNameLP;
+import org.argeo.cms.e4.users.providers.DomainNameLP;
+import org.argeo.cms.e4.users.providers.MailLP;
+import org.argeo.cms.e4.users.providers.UserNameLP;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.LdapObjs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.jface.dialogs.IPageChangeProvider;
+import org.eclipse.jface.dialogs.IPageChangedListener;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.PageChangedEvent;
+import org.eclipse.jface.wizard.IWizardContainer;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Wizard to update users */
+public class UserBatchUpdateWizard extends Wizard {
+
+       private final static Log log = LogFactory.getLog(UserBatchUpdateWizard.class);
+       private UserAdminWrapper userAdminWrapper;
+
+       // pages
+       private ChooseCommandWizardPage chooseCommandPage;
+       private ChooseUsersWizardPage userListPage;
+       private ValidateAndLaunchWizardPage validatePage;
+
+       // Various implemented commands keys
+       private final static String CMD_UPDATE_PASSWORD = "resetPassword";
+       private final static String CMD_UPDATE_EMAIL = "resetEmail";
+       private final static String CMD_GROUP_MEMBERSHIP = "groupMembership";
+
+       private final Map<String, String> commands = new HashMap<String, String>() {
+               private static final long serialVersionUID = 1L;
+               {
+                       put("Reset password(s)", CMD_UPDATE_PASSWORD);
+                       put("Reset email(s)", CMD_UPDATE_EMAIL);
+                       // TODO implement role / group management
+                       // put("Add/Remove from group", CMD_GROUP_MEMBERSHIP);
+               }
+       };
+
+       public UserBatchUpdateWizard(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+
+       @Override
+       public void addPages() {
+               chooseCommandPage = new ChooseCommandWizardPage();
+               addPage(chooseCommandPage);
+               userListPage = new ChooseUsersWizardPage();
+               addPage(userListPage);
+               validatePage = new ValidateAndLaunchWizardPage();
+               addPage(validatePage);
+       }
+
+       @Override
+       public boolean performFinish() {
+               if (!canFinish())
+                       return false;
+               UserTransaction ut = userAdminWrapper.getUserTransaction();
+               try {
+                       if (ut.getStatus() != javax.transaction.Status.STATUS_NO_TRANSACTION
+                                       && !MessageDialog.openConfirm(getShell(), "Existing Transaction",
+                                                       "A user transaction is already existing, " + "are you sure you want to proceed ?"))
+                               return false;
+               } catch (SystemException e) {
+                       throw new CmsException("Cannot get user transaction state " + "before user batch update", e);
+               }
+
+               // We cannot use jobs, user modifications are still meant to be done in
+               // the UIThread
+               // UpdateJob job = null;
+               // if (job != null)
+               // job.schedule();
+
+               if (CMD_UPDATE_PASSWORD.equals(chooseCommandPage.getCommand())) {
+                       char[] newValue = chooseCommandPage.getPwdValue();
+                       if (newValue == null)
+                               throw new CmsException("Password cannot be null or an empty string");
+                       ResetPassword job = new ResetPassword(userAdminWrapper, userListPage.getSelectedUsers(), newValue);
+                       job.doUpdate();
+               } else if (CMD_UPDATE_EMAIL.equals(chooseCommandPage.getCommand())) {
+                       String newValue = chooseCommandPage.getEmailValue();
+                       if (newValue == null)
+                               throw new CmsException("Password cannot be null or an empty string");
+                       ResetEmail job = new ResetEmail(userAdminWrapper, userListPage.getSelectedUsers(), newValue);
+                       job.doUpdate();
+               }
+               return true;
+       }
+
+       public boolean canFinish() {
+               if (this.getContainer().getCurrentPage() == validatePage)
+                       return true;
+               return false;
+       }
+
+       private class ResetPassword {
+               private char[] newPwd;
+               private UserAdminWrapper userAdminWrapper;
+               private List<User> usersToUpdate;
+
+               public ResetPassword(UserAdminWrapper userAdminWrapper, List<User> usersToUpdate, char[] newPwd) {
+                       this.newPwd = newPwd;
+                       this.usersToUpdate = usersToUpdate;
+                       this.userAdminWrapper = userAdminWrapper;
+               }
+
+               @SuppressWarnings("unchecked")
+               protected void doUpdate() {
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       try {
+                               for (User user : usersToUpdate) {
+                                       // the char array is emptied after being used.
+                                       user.getCredentials().put(null, newPwd.clone());
+                               }
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                       } catch (Exception e) {
+                               throw new CmsException("Cannot perform batch update on users", e);
+                       } finally {
+                               UserTransaction ut = userAdminWrapper.getUserTransaction();
+                               try {
+                                       if (ut.getStatus() != javax.transaction.Status.STATUS_NO_TRANSACTION)
+                                               ut.rollback();
+                               } catch (IllegalStateException | SecurityException | SystemException e) {
+                                       log.error("Unable to rollback session in 'finally', " + "the system might be in a dirty state");
+                                       e.printStackTrace();
+                               }
+                       }
+               }
+       }
+
+       private class ResetEmail {
+               private String newEmail;
+               private UserAdminWrapper userAdminWrapper;
+               private List<User> usersToUpdate;
+
+               public ResetEmail(UserAdminWrapper userAdminWrapper, List<User> usersToUpdate, String newEmail) {
+                       this.newEmail = newEmail;
+                       this.usersToUpdate = usersToUpdate;
+                       this.userAdminWrapper = userAdminWrapper;
+               }
+
+               @SuppressWarnings("unchecked")
+               protected void doUpdate() {
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       try {
+                               for (User user : usersToUpdate) {
+                                       // the char array is emptied after being used.
+                                       user.getProperties().put(LdapAttrs.mail.name(), newEmail);
+                               }
+
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               if (!usersToUpdate.isEmpty())
+                                       userAdminWrapper.notifyListeners(
+                                                       new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, usersToUpdate.get(0)));
+                       } catch (Exception e) {
+                               throw new CmsException("Cannot perform batch update on users", e);
+                       } finally {
+                               UserTransaction ut = userAdminWrapper.getUserTransaction();
+                               try {
+                                       if (ut.getStatus() != javax.transaction.Status.STATUS_NO_TRANSACTION)
+                                               ut.rollback();
+                               } catch (IllegalStateException | SecurityException | SystemException e) {
+                                       log.error("Unable to rollback session in finally block, the system might be in a dirty state");
+                                       e.printStackTrace();
+                               }
+                       }
+               }
+       }
+
+       // @SuppressWarnings("unused")
+       // private class AddToGroup extends UpdateJob {
+       // private String groupID;
+       // private Session session;
+       //
+       // public AddToGroup(Session session, List<Node> nodesToUpdate,
+       // String groupID) {
+       // super(session, nodesToUpdate);
+       // this.session = session;
+       // this.groupID = groupID;
+       // }
+       //
+       // protected void doUpdate(Node node) {
+       // log.info("Add/Remove to group actions are not yet implemented");
+       // // TODO implement this
+       // // try {
+       // // throw new CmsException("Not yet implemented");
+       // // } catch (RepositoryException re) {
+       // // throw new CmsException(
+       // // "Unable to update boolean value for node " + node, re);
+       // // }
+       // }
+       // }
+
+       // /**
+       // * Base privileged job that will be run asynchronously to perform the
+       // batch
+       // * update
+       // */
+       // private abstract class UpdateJob extends PrivilegedJob {
+       //
+       // private final UserAdminWrapper userAdminWrapper;
+       // private final List<User> usersToUpdate;
+       //
+       // protected abstract void doUpdate(User user);
+       //
+       // public UpdateJob(UserAdminWrapper userAdminWrapper,
+       // List<User> usersToUpdate) {
+       // super("Perform update");
+       // this.usersToUpdate = usersToUpdate;
+       // this.userAdminWrapper = userAdminWrapper;
+       // }
+       //
+       // @Override
+       // protected IStatus doRun(IProgressMonitor progressMonitor) {
+       // try {
+       // JcrMonitor monitor = new EclipseJcrMonitor(progressMonitor);
+       // int total = usersToUpdate.size();
+       // monitor.beginTask("Performing change", total);
+       // userAdminWrapper.beginTransactionIfNeeded();
+       // for (User user : usersToUpdate) {
+       // doUpdate(user);
+       // monitor.worked(1);
+       // }
+       // userAdminWrapper.getUserTransaction().commit();
+       // } catch (Exception e) {
+       // throw new CmsException(
+       // "Cannot perform batch update on users", e);
+       // } finally {
+       // UserTransaction ut = userAdminWrapper.getUserTransaction();
+       // try {
+       // if (ut.getStatus() != javax.transaction.Status.STATUS_NO_TRANSACTION)
+       // ut.rollback();
+       // } catch (IllegalStateException | SecurityException
+       // | SystemException e) {
+       // log.error("Unable to rollback session in 'finally', "
+       // + "the system might be in a dirty state");
+       // e.printStackTrace();
+       // }
+       // }
+       // return Status.OK_STATUS;
+       // }
+       // }
+
+       // PAGES
+       /**
+        * Displays a combo box that enables user to choose which action to perform
+        */
+       private class ChooseCommandWizardPage extends WizardPage {
+               private static final long serialVersionUID = -8069434295293996633L;
+               private Combo chooseCommandCmb;
+               private Button trueChk;
+               private Text valueTxt;
+               private Text pwdTxt;
+               private Text pwd2Txt;
+
+               public ChooseCommandWizardPage() {
+                       super("Choose a command to run.");
+                       setTitle("Choose a command to run.");
+               }
+
+               @Override
+               public void createControl(Composite parent) {
+                       GridLayout gl = new GridLayout();
+                       Composite container = new Composite(parent, SWT.NO_FOCUS);
+                       container.setLayout(gl);
+
+                       chooseCommandCmb = new Combo(container, SWT.READ_ONLY);
+                       chooseCommandCmb.setLayoutData(EclipseUiUtils.fillWidth());
+                       String[] values = commands.keySet().toArray(new String[0]);
+                       chooseCommandCmb.setItems(values);
+
+                       final Composite bottomPart = new Composite(container, SWT.NO_FOCUS);
+                       bottomPart.setLayoutData(EclipseUiUtils.fillAll());
+                       bottomPart.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       chooseCommandCmb.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       if (getCommand().equals(CMD_UPDATE_PASSWORD))
+                                               populatePasswordCmp(bottomPart);
+                                       else if (getCommand().equals(CMD_UPDATE_EMAIL))
+                                               populateEmailCmp(bottomPart);
+                                       else if (getCommand().equals(CMD_GROUP_MEMBERSHIP))
+                                               populateGroupCmp(bottomPart);
+                                       else
+                                               populateBooleanFlagCmp(bottomPart);
+                                       checkPageComplete();
+                                       bottomPart.layout(true, true);
+                               }
+                       });
+                       setControl(container);
+               }
+
+               private void populateBooleanFlagCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       trueChk = new Button(parent, SWT.CHECK);
+                       trueChk.setText("Do it. (It will to the contrary if unchecked)");
+                       trueChk.setSelection(true);
+                       trueChk.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false));
+               }
+
+               private void populatePasswordCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       Composite body = new Composite(parent, SWT.NO_FOCUS);
+
+                       ModifyListener ml = new ModifyListener() {
+                               private static final long serialVersionUID = -1558726363536729634L;
+
+                               @Override
+                               public void modifyText(ModifyEvent event) {
+                                       checkPageComplete();
+                               }
+                       };
+
+                       body.setLayout(new GridLayout(2, false));
+                       body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       pwdTxt = EclipseUiUtils.createGridLP(body, "New password", ml);
+                       pwd2Txt = EclipseUiUtils.createGridLP(body, "Repeat password", ml);
+               }
+
+               private void populateEmailCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       Composite body = new Composite(parent, SWT.NO_FOCUS);
+
+                       ModifyListener ml = new ModifyListener() {
+                               private static final long serialVersionUID = 2147704227294268317L;
+
+                               @Override
+                               public void modifyText(ModifyEvent event) {
+                                       checkPageComplete();
+                               }
+                       };
+
+                       body.setLayout(new GridLayout(2, false));
+                       body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       valueTxt = EclipseUiUtils.createGridLT(body, "New e-mail", ml);
+               }
+
+               private void checkPageComplete() {
+                       String errorMsg = null;
+                       if (chooseCommandCmb.getSelectionIndex() < 0)
+                               errorMsg = "Please select an action";
+                       else if (CMD_UPDATE_EMAIL.equals(getCommand())) {
+                               if (!valueTxt.getText().matches(UiAdminUtils.EMAIL_PATTERN))
+                                       errorMsg = "Not a valid e-mail address";
+                       } else if (CMD_UPDATE_PASSWORD.equals(getCommand())) {
+                               if (EclipseUiUtils.isEmpty(pwdTxt.getText()) || pwdTxt.getText().length() < 4)
+                                       errorMsg = "Please enter a password that is at least 4 character long";
+                               else if (!pwdTxt.getText().equals(pwd2Txt.getText()))
+                                       errorMsg = "Passwords are different";
+                       }
+                       if (EclipseUiUtils.notEmpty(errorMsg)) {
+                               setMessage(errorMsg, WizardPage.ERROR);
+                               setPageComplete(false);
+                       } else {
+                               setMessage("Page complete, you can proceed to user choice", WizardPage.INFORMATION);
+                               setPageComplete(true);
+                       }
+
+                       getContainer().updateButtons();
+               }
+
+               private void populateGroupCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       trueChk = new Button(parent, SWT.CHECK);
+                       trueChk.setText("Add to group. (It will remove user(s) from the " + "corresponding group if unchecked)");
+                       trueChk.setSelection(true);
+                       trueChk.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false));
+               }
+
+               protected String getCommand() {
+                       return commands.get(chooseCommandCmb.getItem(chooseCommandCmb.getSelectionIndex()));
+               }
+
+               protected String getCommandLbl() {
+                       return chooseCommandCmb.getItem(chooseCommandCmb.getSelectionIndex());
+               }
+
+               @SuppressWarnings("unused")
+               protected boolean getBoleanValue() {
+                       // FIXME this is not consistent and will lead to errors.
+                       if ("argeo:enabled".equals(getCommand()))
+                               return trueChk.getSelection();
+                       else
+                               return !trueChk.getSelection();
+               }
+
+               @SuppressWarnings("unused")
+               protected String getStringValue() {
+                       String value = null;
+                       if (valueTxt != null) {
+                               value = valueTxt.getText();
+                               if ("".equals(value.trim()))
+                                       value = null;
+                       }
+                       return value;
+               }
+
+               protected char[] getPwdValue() {
+                       // We do not directly reset the password text fields: There is no
+                       // need to over secure this process: setting a pwd to multi users
+                       // at the same time is anyhow a bad practice and should be used only
+                       // in test environment or for temporary access
+                       if (pwdTxt == null || pwdTxt.isDisposed())
+                               return null;
+                       else
+                               return pwdTxt.getText().toCharArray();
+               }
+
+               protected String getEmailValue() {
+                       // We do not directly reset the password text fields: There is no
+                       // need to over secure this process: setting a pwd to multi users
+                       // at the same time is anyhow a bad practice and should be used only
+                       // in test environment or for temporary access
+                       if (valueTxt == null || valueTxt.isDisposed())
+                               return null;
+                       else
+                               return valueTxt.getText();
+               }
+       }
+
+       /**
+        * Displays a list of users with a check box to be able to choose some of
+        * them
+        */
+       private class ChooseUsersWizardPage extends WizardPage implements IPageChangedListener {
+               private static final long serialVersionUID = 7651807402211214274L;
+               private ChooseUserTableViewer userTableCmp;
+
+               public ChooseUsersWizardPage() {
+                       super("Choose Users");
+                       setTitle("Select users who will be impacted");
+               }
+
+               @Override
+               public void createControl(Composite parent) {
+                       Composite pageCmp = new Composite(parent, SWT.NONE);
+                       pageCmp.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       // Define the displayed columns
+                       List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+                       columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150));
+                       columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150));
+                       columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200));
+
+                       // Only show technical DN to admin
+                       if (CurrentUser.isInRole(NodeConstants.ROLE_ADMIN))
+                               columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300));
+
+                       userTableCmp = new ChooseUserTableViewer(pageCmp, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+                       userTableCmp.setLayoutData(EclipseUiUtils.fillAll());
+                       userTableCmp.setColumnDefinitions(columnDefs);
+                       userTableCmp.populate(true, true);
+                       userTableCmp.refresh();
+
+                       setControl(pageCmp);
+
+                       // Add listener to update message when shown
+                       final IWizardContainer wContainer = this.getContainer();
+                       if (wContainer instanceof IPageChangeProvider) {
+                               ((IPageChangeProvider) wContainer).addPageChangedListener(this);
+                       }
+
+               }
+
+               @Override
+               public void pageChanged(PageChangedEvent event) {
+                       if (event.getSelectedPage() == this) {
+                               String msg = "Chosen batch action: " + chooseCommandPage.getCommandLbl();
+                               ((WizardPage) event.getSelectedPage()).setMessage(msg);
+                       }
+               }
+
+               protected List<User> getSelectedUsers() {
+                       return userTableCmp.getSelectedUsers();
+               }
+
+               private class ChooseUserTableViewer extends LdifUsersTable {
+                       private static final long serialVersionUID = 5080437561015853124L;
+                       private final String[] knownProps = { LdapAttrs.uid.name(), LdapAttrs.DN, LdapAttrs.cn.name(),
+                                       LdapAttrs.givenName.name(), LdapAttrs.sn.name(), LdapAttrs.mail.name() };
+
+                       public ChooseUserTableViewer(Composite parent, int style) {
+                               super(parent, style);
+                       }
+
+                       @Override
+                       protected List<User> listFilteredElements(String filter) {
+                               Role[] roles;
+
+                               try {
+                                       StringBuilder builder = new StringBuilder();
+
+                                       StringBuilder tmpBuilder = new StringBuilder();
+                                       if (EclipseUiUtils.notEmpty(filter))
+                                               for (String prop : knownProps) {
+                                                       tmpBuilder.append("(");
+                                                       tmpBuilder.append(prop);
+                                                       tmpBuilder.append("=*");
+                                                       tmpBuilder.append(filter);
+                                                       tmpBuilder.append("*)");
+                                               }
+                                       if (tmpBuilder.length() > 1) {
+                                               builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=")
+                                                               .append(LdapObjs.inetOrgPerson.name()).append(")(|");
+                                               builder.append(tmpBuilder.toString());
+                                               builder.append("))");
+                                       } else
+                                               builder.append("(").append(LdapAttrs.objectClass.name()).append("=")
+                                                               .append(LdapObjs.inetOrgPerson.name()).append(")");
+                                       roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString());
+                               } catch (InvalidSyntaxException e) {
+                                       throw new CmsException("Unable to get roles with filter: " + filter, e);
+                               }
+                               List<User> users = new ArrayList<User>();
+                               for (Role role : roles)
+                                       // Prevent current logged in user to perform batch on
+                                       // himself
+                                       if (!UserAdminUtils.isCurrentUser((User) role))
+                                               users.add((User) role);
+                               return users;
+                       }
+               }
+       }
+
+       /** Summary of input data before launching the process */
+       private class ValidateAndLaunchWizardPage extends WizardPage implements IPageChangedListener {
+               private static final long serialVersionUID = 7098918351451743853L;
+               private ChosenUsersTableViewer userTableCmp;
+
+               public ValidateAndLaunchWizardPage() {
+                       super("Validate and launch");
+                       setTitle("Validate and launch");
+               }
+
+               @Override
+               public void createControl(Composite parent) {
+                       Composite pageCmp = new Composite(parent, SWT.NO_FOCUS);
+                       pageCmp.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+                       columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150));
+                       columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150));
+                       columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200));
+                       // Only show technical DN to admin
+                       if (CurrentUser.isInRole(NodeConstants.ROLE_ADMIN))
+                               columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300));
+                       userTableCmp = new ChosenUsersTableViewer(pageCmp, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+                       userTableCmp.setLayoutData(EclipseUiUtils.fillAll());
+                       userTableCmp.setColumnDefinitions(columnDefs);
+                       userTableCmp.populate(false, false);
+                       userTableCmp.refresh();
+                       setControl(pageCmp);
+                       // Add listener to update message when shown
+                       final IWizardContainer wContainer = this.getContainer();
+                       if (wContainer instanceof IPageChangeProvider) {
+                               ((IPageChangeProvider) wContainer).addPageChangedListener(this);
+                       }
+               }
+
+               @Override
+               public void pageChanged(PageChangedEvent event) {
+                       if (event.getSelectedPage() == this) {
+                               @SuppressWarnings({ "unchecked", "rawtypes" })
+                               Object[] values = ((ArrayList) userListPage.getSelectedUsers())
+                                               .toArray(new Object[userListPage.getSelectedUsers().size()]);
+                               userTableCmp.getTableViewer().setInput(values);
+                               String msg = "Following batch action: [" + chooseCommandPage.getCommandLbl()
+                                               + "] will be perfomed on the users listed below.\n";
+                               // + "Are you sure you want to proceed?";
+                               setMessage(msg);
+                       }
+               }
+
+               private class ChosenUsersTableViewer extends LdifUsersTable {
+                       private static final long serialVersionUID = 7814764735794270541L;
+
+                       public ChosenUsersTableViewer(Composite parent, int style) {
+                               super(parent, style);
+                       }
+
+                       @Override
+                       protected List<User> listFilteredElements(String filter) {
+                               return userListPage.getSelectedUsers();
+                       }
+               }
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserEditor.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserEditor.java
new file mode 100644 (file)
index 0000000..4ac9646
--- /dev/null
@@ -0,0 +1,551 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.users;
+
+import static org.argeo.cms.util.UserAdminUtils.getProperty;
+import static org.argeo.naming.LdapAttrs.cn;
+import static org.argeo.naming.LdapAttrs.givenName;
+import static org.argeo.naming.LdapAttrs.mail;
+import static org.argeo.naming.LdapAttrs.sn;
+import static org.argeo.naming.LdapAttrs.uid;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.e4.users.providers.CommonNameLP;
+import org.argeo.cms.e4.users.providers.DomainNameLP;
+import org.argeo.cms.e4.users.providers.RoleIconLP;
+import org.argeo.cms.e4.users.providers.UserFilter;
+import org.argeo.cms.ui.eclipse.forms.AbstractFormPart;
+//import org.argeo.cms.ui.eclipse.forms.FormToolkit;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TrayDialog;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Display/edit the properties of a given user */
+public class UserEditor extends AbstractRoleEditor implements ArgeoNames {
+       // final static String ID = "UserEditor.mainPage";
+
+       @Inject
+       private EPartService partService;
+
+       // private final UserEditor editor;
+       // private UserAdminWrapper userAdminWrapper;
+
+       // Local configuration
+       // private final int PRE_TITLE_INDENT = 10;
+
+       // public UserMainPage(FormEditor editor, UserAdminWrapper userAdminWrapper) {
+       // super(editor, ID, "Main");
+       // this.editor = (UserEditor) editor;
+       // this.userAdminWrapper = userAdminWrapper;
+       // }
+
+       // protected void createFormContent(final IManagedForm mf) {
+       // ScrolledForm form = mf.getForm();
+       // Composite body = form.getBody();
+       // GridLayout mainLayout = new GridLayout();
+       // // mainLayout.marginRight = 10;
+       // body.setLayout(mainLayout);
+       // User user = editor.getDisplayedUser();
+       // appendOverviewPart(body, user);
+       // // Remove to ability to force the password for his own user. The user
+       // // must then use the change pwd feature
+       // appendMemberOfPart(body, user);
+       // }
+
+       @Override
+       protected void createUi(Composite body) {
+               // Composite body = new Composite(parent, SWT.BORDER);
+               GridLayout mainLayout = new GridLayout();
+               // mainLayout.marginRight = 10;
+               body.setLayout(mainLayout);
+               // body.getParent().setLayout(new GridLayout());
+               // body.setLayoutData(CmsUtils.fillAll());
+               User user = getDisplayedUser();
+               appendOverviewPart(body, user);
+               // Remove to ability to force the password for his own user. The user
+               // must then use the change pwd feature
+               appendMemberOfPart(body, user);
+       }
+
+       /** Creates the general section */
+       private void appendOverviewPart(final Composite parent, final User user) {
+               // FormToolkit tk = getManagedForm().getToolkit();
+
+               // Section section = tk.createSection(parent, SWT.NO_FOCUS);
+               // GridData gd = EclipseUiUtils.fillWidth();
+               // // gd.verticalAlignment = PRE_TITLE_INDENT;
+               // section.setLayoutData(gd);
+               Composite body = new Composite(parent, SWT.NONE);
+               body.setLayoutData(EclipseUiUtils.fillWidth());
+               // section.setClient(body);
+               // body.setLayout(new GridLayout(6, false));
+               body.setLayout(new GridLayout(2, false));
+
+               Text commonName = createReadOnlyLT(body, "Name", getProperty(user, cn));
+               Text distinguishedName = createReadOnlyLT(body, "Login", getProperty(user, uid));
+               Text firstName = createLT(body, "First name", getProperty(user, givenName));
+               Text lastName = createLT(body, "Last name", getProperty(user, sn));
+               Text email = createLT(body, "Email", getProperty(user, mail));
+
+               Link resetPwdLk = new Link(body, SWT.NONE);
+               if (!UserAdminUtils.isCurrentUser(user)) {
+                       resetPwdLk.setText("<a>Reset password</a>");
+               }
+               resetPwdLk.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 2, 1));
+
+               // create form part (controller)
+               AbstractFormPart part = new AbstractFormPart() {
+                       private MainInfoListener listener;
+
+                       @Override
+                       public void initialize(IManagedForm form) {
+                               super.initialize(form);
+                               listener = new MainInfoListener(parent.getDisplay(), this);
+                               userAdminWrapper.addListener(listener);
+                       }
+
+                       @Override
+                       public void dispose() {
+                               userAdminWrapper.removeListener(listener);
+                               super.dispose();
+                       }
+
+                       @SuppressWarnings("unchecked")
+                       public void commit(boolean onSave) {
+                               // TODO Sanity checks (mail validity...)
+                               user.getProperties().put(LdapAttrs.givenName.name(), firstName.getText());
+                               user.getProperties().put(LdapAttrs.sn.name(), lastName.getText());
+                               user.getProperties().put(LdapAttrs.cn.name(), commonName.getText());
+                               user.getProperties().put(LdapAttrs.mail.name(), email.getText());
+                               super.commit(onSave);
+                       }
+
+                       @Override
+                       public void refresh() {
+                               distinguishedName.setText(UserAdminUtils.getProperty(user, LdapAttrs.uid.name()));
+                               commonName.setText(UserAdminUtils.getProperty(user, LdapAttrs.cn.name()));
+                               firstName.setText(UserAdminUtils.getProperty(user, LdapAttrs.givenName.name()));
+                               lastName.setText(UserAdminUtils.getProperty(user, LdapAttrs.sn.name()));
+                               email.setText(UserAdminUtils.getProperty(user, LdapAttrs.mail.name()));
+                               refreshFormTitle(user);
+                               super.refresh();
+                       }
+               };
+
+               // Improve this: automatically generate CN when first or last name
+               // changes
+               ModifyListener cnML = new ModifyListener() {
+                       private static final long serialVersionUID = 4298649222869835486L;
+
+                       @Override
+                       public void modifyText(ModifyEvent event) {
+                               String first = firstName.getText();
+                               String last = lastName.getText();
+                               String cn = first.trim() + " " + last.trim() + " ";
+                               cn = cn.trim();
+                               commonName.setText(cn);
+                               // getManagedForm().getForm().setText(cn);
+                               updateEditorTitle(cn);
+                       }
+               };
+               firstName.addModifyListener(cnML);
+               lastName.addModifyListener(cnML);
+
+               ModifyListener defaultListener = new FormPartML(part);
+               firstName.addModifyListener(defaultListener);
+               lastName.addModifyListener(defaultListener);
+               email.addModifyListener(defaultListener);
+
+               if (!UserAdminUtils.isCurrentUser(user))
+                       resetPwdLk.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = 5881800534589073787L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       new ChangePasswordDialog(user, "Reset password").open();
+                               }
+                       });
+
+               getManagedForm().addPart(part);
+       }
+
+       private class ChangePasswordDialog extends TrayDialog {
+               private static final long serialVersionUID = 2843538207460082349L;
+
+               private User user;
+               private Text password1;
+               private Text password2;
+               private String title;
+               // private FormToolkit tk;
+
+               public ChangePasswordDialog(User user, String title) {
+                       super(Display.getDefault().getActiveShell());
+                       // this.tk = tk;
+                       this.user = user;
+                       this.title = title;
+               }
+
+               protected Control createDialogArea(Composite parent) {
+                       Composite dialogarea = (Composite) super.createDialogArea(parent);
+                       dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       Composite body = new Composite(dialogarea, SWT.NO_FOCUS);
+                       body.setLayoutData(EclipseUiUtils.fillAll());
+                       GridLayout layout = new GridLayout(2, false);
+                       body.setLayout(layout);
+
+                       password1 = createLP(body, "New password", "");
+                       password2 = createLP(body, "Repeat password", "");
+                       parent.pack();
+                       return body;
+               }
+
+               @SuppressWarnings("unchecked")
+               @Override
+               protected void okPressed() {
+                       String msg = null;
+
+                       if (password1.getText().equals(""))
+                               msg = "Password cannot be empty";
+                       else if (password1.getText().equals(password2.getText())) {
+                               char[] newPassword = password1.getText().toCharArray();
+                               // userAdminWrapper.beginTransactionIfNeeded();
+                               userAdminWrapper.beginTransactionIfNeeded();
+                               user.getCredentials().put(null, newPassword);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               super.okPressed();
+                       } else {
+                               msg = "Passwords are not equals";
+                       }
+
+                       if (EclipseUiUtils.notEmpty(msg))
+                               MessageDialog.openError(getParentShell(), "Cannot reset pasword", msg);
+               }
+
+               protected void configureShell(Shell shell) {
+                       super.configureShell(shell);
+                       shell.setText(title);
+               }
+       }
+
+       private LdifUsersTable appendMemberOfPart(final Composite parent, User user) {
+               // Section section = addSection(tk, parent, "Roles");
+               // Composite body = (Composite) section.getClient();
+               // Composite body= parent;
+               Composite body = new Composite(parent, SWT.BORDER);
+               body.setLayout(new GridLayout());
+               body.setLayoutData(CmsUtils.fillAll());
+
+               // boolean isAdmin = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+
+               // Displayed columns
+               List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+               columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24));
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+               columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 100));
+               // Only show technical DN to administrators
+               // if (isAdmin)
+               // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name",
+               // 300));
+
+               // Create and configure the table
+               final LdifUsersTable userViewerCmp = new MyUserTableViewer(body, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL, user);
+
+               userViewerCmp.setColumnDefinitions(columnDefs);
+               // if (isAdmin)
+               // userViewerCmp.populateWithStaticFilters(false, false);
+               // else
+               userViewerCmp.populate(true, false);
+               GridData gd = EclipseUiUtils.fillAll();
+               gd.heightHint = 500;
+               userViewerCmp.setLayoutData(gd);
+
+               // Controllers
+               TableViewer userViewer = userViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService));
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               GroupDropListener dropL = new GroupDropListener(userAdminWrapper, userViewer, user);
+               userViewer.addDropSupport(operations, tt, dropL);
+
+               AbstractFormPart part = new AbstractFormPart() {
+
+                       private GroupChangeListener listener;
+
+                       @Override
+                       public void initialize(IManagedForm form) {
+                               super.initialize(form);
+                               listener = new GroupChangeListener(parent.getDisplay(), this);
+                               userAdminWrapper.addListener(listener);
+                       }
+
+                       public void commit(boolean onSave) {
+                               super.commit(onSave);
+                       }
+
+                       @Override
+                       public void dispose() {
+                               userAdminWrapper.removeListener(listener);
+                               super.dispose();
+                       }
+
+                       @Override
+                       public void refresh() {
+                               userViewerCmp.refresh();
+                               super.refresh();
+                       }
+               };
+               getManagedForm().addPart(part);
+               // addRemoveAbitily(body, userViewer, user);
+               // userViewerCmp.refresh();
+               String tooltip = "Remove " + UserAdminUtils.getUserLocalId(user.getName()) + " from the below selected groups";
+               Action action = new RemoveMembershipAction(userViewer, user, tooltip, SecurityAdminImages.ICON_REMOVE_DESC);
+               ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+               ToolBar toolBar = toolBarManager.createControl(body);
+               toolBar.setLayoutData(CmsUtils.fillWidth());
+               toolBarManager.add(action);
+               toolBarManager.update(true);
+               return userViewerCmp;
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 2653790051461237329L;
+
+               private Button showSystemRoleBtn;
+
+               private final User user;
+               private final UserFilter userFilter;
+
+               public MyUserTableViewer(Composite parent, int style, User user) {
+                       super(parent, style, true);
+                       this.user = user;
+                       userFilter = new UserFilter();
+               }
+
+               protected void populateStaticFilters(Composite staticFilterCmp) {
+                       staticFilterCmp.setLayout(new GridLayout());
+                       showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK);
+                       showSystemRoleBtn.setText("Show system roles");
+                       boolean showSysRole = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+                       showSystemRoleBtn.setSelection(showSysRole);
+                       userFilter.setShowSystemRole(showSysRole);
+                       showSystemRoleBtn.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = -7033424592697691676L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       userFilter.setShowSystemRole(showSystemRoleBtn.getSelection());
+                                       refresh();
+                               }
+                       });
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       List<User> users = (List<User>) getFlatGroups(null);
+                       List<User> filteredUsers = new ArrayList<User>();
+                       if (users.contains(user))
+                               users.remove(user);
+                       userFilter.setSearchText(filter);
+                       for (User user : users)
+                               if (userFilter.select(null, null, user))
+                                       filteredUsers.add(user);
+                       return filteredUsers;
+               }
+       }
+
+       // private void addRemoveAbility(Composite parent, TableViewer userViewer, User
+       // user) {
+       // // Section section = sectionPart.getSection();
+       // ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+       // ToolBar toolbar = toolBarManager.createControl(parent);
+       // final Cursor handCursor = new Cursor(Display.getCurrent(), SWT.CURSOR_HAND);
+       // toolbar.setCursor(handCursor);
+       // toolbar.addDisposeListener(new DisposeListener() {
+       // private static final long serialVersionUID = 3882131405820522925L;
+       //
+       // public void widgetDisposed(DisposeEvent e) {
+       // if ((handCursor != null) && (handCursor.isDisposed() == false)) {
+       // handCursor.dispose();
+       // }
+       // }
+       // });
+       //
+       // String tooltip = "Remove " + UserAdminUtils.getUserLocalId(user.getName()) +
+       // " from the below selected groups";
+       // Action action = new RemoveMembershipAction(userViewer, user, tooltip,
+       // SecurityAdminImages.ICON_REMOVE_DESC);
+       // toolBarManager.add(action);
+       // toolBarManager.update(true);
+       // // section.setTextClient(toolbar);
+       // }
+
+       private class RemoveMembershipAction extends Action {
+               private static final long serialVersionUID = -1337713097184522588L;
+
+               private final TableViewer userViewer;
+               private final User user;
+
+               RemoveMembershipAction(TableViewer userViewer, User user, String name, ImageDescriptor img) {
+                       super(name, img);
+                       this.userViewer = userViewer;
+                       this.user = user;
+               }
+
+               @Override
+               public void run() {
+                       ISelection selection = userViewer.getSelection();
+                       if (selection.isEmpty())
+                               return;
+
+                       @SuppressWarnings("unchecked")
+                       Iterator<Group> it = ((IStructuredSelection) selection).iterator();
+                       List<Group> groups = new ArrayList<Group>();
+                       while (it.hasNext()) {
+                               Group currGroup = it.next();
+                               groups.add(currGroup);
+                       }
+
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       for (Group group : groups) {
+                               group.removeMember(user);
+                       }
+                       userAdminWrapper.commitOrNotifyTransactionStateChange();
+                       for (Group group : groups) {
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+                       }
+               }
+       }
+
+       /**
+        * Defines the table as being a potential target to add group memberships
+        * (roles) to this user
+        */
+       private class GroupDropListener extends ViewerDropAdapter {
+               private static final long serialVersionUID = 2893468717831451621L;
+
+               private final UserAdminWrapper myUserAdminWrapper;
+               private final User myUser;
+
+               public GroupDropListener(UserAdminWrapper userAdminWrapper, Viewer userViewer, User user) {
+                       super(userViewer);
+                       this.myUserAdminWrapper = userAdminWrapper;
+                       this.myUser = user;
+               }
+
+               @Override
+               public boolean validateDrop(Object target, int operation, TransferData transferType) {
+                       // Target is always OK in a list only view
+                       // TODO check if not a string
+                       boolean validDrop = true;
+                       return validDrop;
+               }
+
+               @Override
+               public void drop(DropTargetEvent event) {
+                       String name = (String) event.data;
+                       UserAdmin myUserAdmin = myUserAdminWrapper.getUserAdmin();
+                       Role role = myUserAdmin.getRole(name);
+                       // TODO this check should be done before.
+                       if (role.getType() == Role.GROUP) {
+                               // TODO check if the user is already member of this group
+
+                               myUserAdminWrapper.beginTransactionIfNeeded();
+                               Group group = (Group) role;
+                               group.addMember(myUser);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               myUserAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+                       }
+                       super.drop(event);
+               }
+
+               @Override
+               public boolean performDrop(Object data) {
+                       // userTableViewerCmp.refresh();
+                       return true;
+               }
+       }
+
+       // LOCAL HELPERS
+       private void refreshFormTitle(User group) {
+               // getManagedForm().getForm().setText(UserAdminUtils.getProperty(group,
+               // LdapAttrs.cn.name()));
+       }
+
+       /** Appends a section with a title */
+       // private Section addSection(FormToolkit tk, Composite parent, String title) {
+       // Section section = tk.createSection(parent, Section.TITLE_BAR);
+       // GridData gd = EclipseUiUtils.fillWidth();
+       // gd.verticalAlignment = PRE_TITLE_INDENT;
+       // section.setLayoutData(gd);
+       // section.setText(title);
+       // // section.getMenu().setVisible(true);
+       //
+       // Composite body = tk.createComposite(section, SWT.WRAP);
+       // body.setLayoutData(EclipseUiUtils.fillAll());
+       // section.setClient(body);
+       //
+       // return section;
+       // }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserTableDefaultDClickListener.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UserTableDefaultDClickListener.java
new file mode 100644 (file)
index 0000000..a9a4ede
--- /dev/null
@@ -0,0 +1,39 @@
+package org.argeo.cms.e4.users;
+
+import org.argeo.cms.e4.CmsE4Utils;
+import org.argeo.naming.LdapAttrs;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Default double click listener for the various user tables, will open the
+ * clicked item in the editor
+ */
+public class UserTableDefaultDClickListener implements IDoubleClickListener {
+       private final EPartService partService;
+
+       public UserTableDefaultDClickListener(EPartService partService) {
+               this.partService = partService;
+       }
+
+       public void doubleClick(DoubleClickEvent evt) {
+               if (evt.getSelection().isEmpty())
+                       return;
+               Object obj = ((IStructuredSelection) evt.getSelection()).getFirstElement();
+               User user = (User) obj;
+
+               String editorId = getEditorId(user);
+               CmsE4Utils.openEditor(partService, editorId, LdapAttrs.uid.name(), user.getName());
+       }
+
+       protected String getEditorId(User user) {
+               if (user instanceof Group)
+                       return "org.argeo.cms.e4.partdescriptor.groupEditor";
+               else
+                       return "org.argeo.cms.e4.partdescriptor.userEditor";
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UsersView.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/UsersView.java
new file mode 100644 (file)
index 0000000..fb57a62
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.users;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.inject.Inject;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.e4.users.providers.CommonNameLP;
+import org.argeo.cms.e4.users.providers.DomainNameLP;
+import org.argeo.cms.e4.users.providers.MailLP;
+import org.argeo.cms.e4.users.providers.UserDragListener;
+import org.argeo.cms.e4.users.providers.UserNameLP;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.LdapObjs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.e4.ui.di.Focus;
+import org.eclipse.e4.ui.workbench.modeling.EPartService;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** List all users with filter - based on Ldif userAdmin */
+public class UsersView implements ArgeoNames {
+       // private final static Log log = LogFactory.getLog(UsersView.class);
+
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".usersView";
+
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+       @Inject
+       private EPartService partService;
+
+       // UI Objects
+       private LdifUsersTable userTableViewerCmp;
+       private TableViewer userViewer;
+       private List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+
+       private UserAdminListener listener;
+
+       @PostConstruct
+       public void createPartControl(Composite parent, ESelectionService selectionService) {
+
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+               // Define the displayed columns
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150));
+               columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150));
+               columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200));
+               // Only show technical DN to admin
+               if (CurrentUser.isInRole(NodeConstants.ROLE_ADMIN))
+                       columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300));
+
+               // Create and configure the table
+               userTableViewerCmp = new MyUserTableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+               userTableViewerCmp.setLayoutData(EclipseUiUtils.fillAll());
+               userTableViewerCmp.setColumnDefinitions(columnDefs);
+               userTableViewerCmp.populate(true, false);
+
+               // Links
+               userViewer = userTableViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener(partService));
+               userViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+
+                       @Override
+                       public void selectionChanged(SelectionChangedEvent event) {
+                               IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+                               selectionService.setSelection(selection.toList());
+                       }
+               });
+               // getViewSite().setSelectionProvider(userViewer);
+
+               // Really?
+               userTableViewerCmp.refresh();
+
+               // Drag and drop
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               userViewer.addDragSupport(operations, tt, new UserDragListener(userViewer));
+
+               // Register a useradmin listener
+               listener = new MyUiUAListener(parent.getDisplay());
+               userAdminWrapper.addListener(listener);
+       }
+
+       private class MyUiUAListener extends UiUserAdminListener {
+               public MyUiUAListener(Display display) {
+                       super(display);
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       if (userViewer != null && !userViewer.getTable().isDisposed())
+                               refresh();
+               }
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private final String[] knownProps = { LdapAttrs.DN, LdapAttrs.uid.name(), LdapAttrs.cn.name(),
+                               LdapAttrs.givenName.name(), LdapAttrs.sn.name(), LdapAttrs.mail.name() };
+
+               public MyUserTableViewer(Composite parent, int style) {
+                       super(parent, style);
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       Role[] roles;
+
+                       try {
+                               StringBuilder builder = new StringBuilder();
+
+                               StringBuilder tmpBuilder = new StringBuilder();
+                               if (EclipseUiUtils.notEmpty(filter))
+                                       for (String prop : knownProps) {
+                                               tmpBuilder.append("(");
+                                               tmpBuilder.append(prop);
+                                               tmpBuilder.append("=*");
+                                               tmpBuilder.append(filter);
+                                               tmpBuilder.append("*)");
+                                       }
+                               if (tmpBuilder.length() > 1) {
+                                       builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=")
+                                                       .append(LdapObjs.inetOrgPerson.name()).append(")(|");
+                                       builder.append(tmpBuilder.toString());
+                                       builder.append("))");
+                               } else
+                                       builder.append("(").append(LdapAttrs.objectClass.name()).append("=")
+                                                       .append(LdapObjs.inetOrgPerson.name()).append(")");
+                               roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString());
+                       } catch (InvalidSyntaxException e) {
+                               throw new CmsException("Unable to get roles with filter: " + filter, e);
+                       }
+                       List<User> users = new ArrayList<User>();
+                       for (Role role : roles)
+                               // if (role.getType() == Role.USER && role.getType() !=
+                               // Role.GROUP)
+                               users.add((User) role);
+                       return users;
+               }
+       }
+
+       public void refresh() {
+               userTableViewerCmp.refresh();
+       }
+
+       // Override generic view methods
+       @PreDestroy
+       public void dispose() {
+               userAdminWrapper.removeListener(listener);
+       }
+
+       @Focus
+       public void setFocus() {
+               userTableViewerCmp.setFocus();
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/DeleteGroups.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/DeleteGroups.java
new file mode 100644 (file)
index 0000000..561468d
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.users.handlers;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.argeo.cms.e4.users.GroupsView;
+import org.argeo.cms.e4.users.UserAdminWrapper;
+import org.argeo.cms.util.UserAdminUtils;
+import org.eclipse.e4.core.di.annotations.CanExecute;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Delete the selected groups */
+public class DeleteGroups {
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID +
+       // ".deleteGroups";
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+
+       @Inject
+       ESelectionService selectionService;
+
+       @SuppressWarnings("unchecked")
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               // ISelection selection = null;// HandlerUtil.getCurrentSelection(event);
+               // if (selection.isEmpty())
+               // return null;
+               //
+               // List<Group> groups = new ArrayList<Group>();
+               // Iterator<Group> it = ((IStructuredSelection) selection).iterator();
+
+               List<Group> selection = (List<Group>) selectionService.getSelection();
+               if (selection == null)
+                       return;
+
+               StringBuilder builder = new StringBuilder();
+               for (Group group : selection) {
+                       Group currGroup = group;
+                       String groupName = UserAdminUtils.getUserLocalId(currGroup.getName());
+                       // TODO add checks
+                       builder.append(groupName).append("; ");
+                       // groups.add(currGroup);
+               }
+
+               if (!MessageDialog.openQuestion(Display.getCurrent().getActiveShell(), "Delete Groups", "Are you sure that you "
+                               + "want to delete these groups?\n" + builder.substring(0, builder.length() - 2)))
+                       return;
+
+               userAdminWrapper.beginTransactionIfNeeded();
+               UserAdmin userAdmin = userAdminWrapper.getUserAdmin();
+               // IWorkbenchPage iwp =
+               // HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
+               for (Group group : selection) {
+                       String groupName = group.getName();
+                       // TODO find a way to close the editor cleanly if opened. Cannot be
+                       // done through the UserAdminListeners, it causes a
+                       // java.util.ConcurrentModificationException because disposing the
+                       // editor unregisters and disposes the listener
+                       // IEditorPart part = iwp.findEditor(new UserEditorInput(groupName));
+                       // if (part != null)
+                       // iwp.closeEditor(part, false);
+                       userAdmin.removeRole(groupName);
+               }
+               userAdminWrapper.commitOrNotifyTransactionStateChange();
+
+               // Update the view
+               for (Group group : selection) {
+                       userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, group));
+               }
+
+               // return null;
+       }
+
+       @CanExecute
+       public boolean canExecute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               return part.getObject() instanceof GroupsView && selectionService.getSelection() != null;
+       }
+
+       /* DEPENDENCY INJECTION */
+       // public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+       // this.userAdminWrapper = userAdminWrapper;
+       // }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/DeleteUsers.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/DeleteUsers.java
new file mode 100644 (file)
index 0000000..b70312b
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.users.handlers;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.argeo.cms.e4.users.UserAdminWrapper;
+import org.argeo.cms.e4.users.UsersView;
+import org.argeo.cms.util.UserAdminUtils;
+import org.eclipse.e4.core.di.annotations.CanExecute;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.e4.ui.model.application.ui.basic.MPart;
+import org.eclipse.e4.ui.services.IServiceConstants;
+import org.eclipse.e4.ui.workbench.modeling.ESelectionService;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Delete the selected users */
+public class DeleteUsers {
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".deleteUsers";
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+
+       @SuppressWarnings("unchecked")
+       @Execute
+       public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               // ISelection selection = null;// HandlerUtil.getCurrentSelection(event);
+               // if (selection.isEmpty())
+               // return null;
+               List<User> selection = (List<User>) selectionService.getSelection();
+               if (selection == null)
+                       return;
+
+//             Iterator<User> it = ((IStructuredSelection) selection).iterator();
+//             List<User> users = new ArrayList<User>();
+               StringBuilder builder = new StringBuilder();
+
+               for(User user:selection) {
+                       User currUser = user;
+//                     User currUser = it.next();
+                       String userName = UserAdminUtils.getUserLocalId(currUser.getName());
+                       if (UserAdminUtils.isCurrentUser(currUser)) {
+                               MessageDialog.openError(Display.getCurrent().getActiveShell(), "Deletion forbidden",
+                                               "You cannot delete your own user this way.");
+                               return;
+                       }
+                       builder.append(userName).append("; ");
+//                     users.add(currUser);
+               }
+
+               if (!MessageDialog.openQuestion(Display.getCurrent().getActiveShell(), "Delete Users",
+                               "Are you sure that you want to delete these users?\n" + builder.substring(0, builder.length() - 2)))
+                       return;
+
+               userAdminWrapper.beginTransactionIfNeeded();
+               UserAdmin userAdmin = userAdminWrapper.getUserAdmin();
+               // IWorkbenchPage iwp =
+               // HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
+
+               for (User user : selection) {
+                       String userName = user.getName();
+                       // TODO find a way to close the editor cleanly if opened. Cannot be
+                       // done through the UserAdminListeners, it causes a
+                       // java.util.ConcurrentModificationException because disposing the
+                       // editor unregisters and disposes the listener
+                       // IEditorPart part = iwp.findEditor(new UserEditorInput(userName));
+                       // if (part != null)
+                       // iwp.closeEditor(part, false);
+                       userAdmin.removeRole(userName);
+               }
+               userAdminWrapper.commitOrNotifyTransactionStateChange();
+
+               for (User user : selection) {
+                       userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, user));
+               }
+       }
+
+       @CanExecute
+       public boolean canExecute(@Named(IServiceConstants.ACTIVE_PART) MPart part, ESelectionService selectionService) {
+               return part.getObject() instanceof UsersView && selectionService.getSelection() != null;
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewGroup.java
new file mode 100644 (file)
index 0000000..7a8c8bb
--- /dev/null
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.users.handlers;
+
+import java.util.Dictionary;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.e4.users.UserAdminWrapper;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.osgi.useradmin.UserAdminConf;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Create a new group */
+public class NewGroup {
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".newGroup";
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+
+       @Execute
+       public Object execute() {
+               NewGroupWizard newGroupWizard = new NewGroupWizard();
+               newGroupWizard.setWindowTitle("Group creation");
+               WizardDialog dialog = new WizardDialog(Display.getCurrent().getActiveShell(), newGroupWizard);
+               dialog.open();
+               return null;
+       }
+
+       private class NewGroupWizard extends Wizard {
+
+               // Pages
+               private MainGroupInfoWizardPage mainGroupInfo;
+
+               // UI fields
+               private Text dNameTxt, commonNameTxt, descriptionTxt;
+               private Combo baseDnCmb;
+
+               public NewGroupWizard() {
+               }
+
+               @Override
+               public void addPages() {
+                       mainGroupInfo = new MainGroupInfoWizardPage();
+                       addPage(mainGroupInfo);
+               }
+
+               @SuppressWarnings({ "rawtypes", "unchecked" })
+               @Override
+               public boolean performFinish() {
+                       if (!canFinish())
+                               return false;
+                       String commonName = commonNameTxt.getText();
+                       try {
+                               userAdminWrapper.beginTransactionIfNeeded();
+                               String dn = getDn(commonName);
+                               Group group = (Group) userAdminWrapper.getUserAdmin().createRole(dn, Role.GROUP);
+                               Dictionary props = group.getProperties();
+                               String descStr = descriptionTxt.getText();
+                               if (EclipseUiUtils.notEmpty(descStr))
+                                       props.put(LdapAttrs.description.name(), descStr);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CREATED, group));
+                               return true;
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot create new group " + commonName, e);
+                               return false;
+                       }
+               }
+
+               private class MainGroupInfoWizardPage extends WizardPage implements FocusListener, ArgeoNames {
+                       private static final long serialVersionUID = -3150193365151601807L;
+
+                       public MainGroupInfoWizardPage() {
+                               super("Main");
+                               setTitle("General information");
+                               setMessage("Please choose a domain, provide a common name " + "and a free description");
+                       }
+
+                       @Override
+                       public void createControl(Composite parent) {
+                               Composite bodyCmp = new Composite(parent, SWT.NONE);
+                               setControl(bodyCmp);
+                               bodyCmp.setLayout(new GridLayout(2, false));
+
+                               dNameTxt = EclipseUiUtils.createGridLT(bodyCmp, "Distinguished name");
+                               dNameTxt.setEnabled(false);
+
+                               baseDnCmb = createGridLC(bodyCmp, "Base DN");
+                               // Initialise before adding the listener to avoid NPE
+                               initialiseDnCmb(baseDnCmb);
+                               baseDnCmb.addFocusListener(this);
+
+                               commonNameTxt = EclipseUiUtils.createGridLT(bodyCmp, "Common name");
+                               commonNameTxt.addFocusListener(this);
+
+                               Label descLbl = new Label(bodyCmp, SWT.LEAD);
+                               descLbl.setText("Description");
+                               descLbl.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false, false));
+                               descriptionTxt = new Text(bodyCmp, SWT.LEAD | SWT.MULTI | SWT.WRAP | SWT.BORDER);
+                               descriptionTxt.setLayoutData(EclipseUiUtils.fillAll());
+                               descriptionTxt.addFocusListener(this);
+
+                               // Initialize buttons
+                               setPageComplete(false);
+                               getContainer().updateButtons();
+                       }
+
+                       @Override
+                       public void focusLost(FocusEvent event) {
+                               String name = commonNameTxt.getText();
+                               if (EclipseUiUtils.isEmpty(name))
+                                       dNameTxt.setText("");
+                               else
+                                       dNameTxt.setText(getDn(name));
+
+                               String message = checkComplete();
+                               if (message != null) {
+                                       setMessage(message, WizardPage.ERROR);
+                                       setPageComplete(false);
+                               } else {
+                                       setMessage("Complete", WizardPage.INFORMATION);
+                                       setPageComplete(true);
+                               }
+                               getContainer().updateButtons();
+                       }
+
+                       @Override
+                       public void focusGained(FocusEvent event) {
+                       }
+
+                       /** @return the error message or null if complete */
+                       protected String checkComplete() {
+                               String name = commonNameTxt.getText();
+
+                               if (name.trim().equals(""))
+                                       return "Common name must not be empty";
+                               Role role = userAdminWrapper.getUserAdmin().getRole(getDn(name));
+                               if (role != null)
+                                       return "Group " + name + " already exists";
+                               return null;
+                       }
+
+                       @Override
+                       public void setVisible(boolean visible) {
+                               super.setVisible(visible);
+                               if (visible)
+                                       if (baseDnCmb.getSelectionIndex() == -1)
+                                               baseDnCmb.setFocus();
+                                       else
+                                               commonNameTxt.setFocus();
+                       }
+               }
+
+               private Map<String, String> getDns() {
+                       return userAdminWrapper.getKnownBaseDns(true);
+               }
+
+               private String getDn(String cn) {
+                       Map<String, String> dns = getDns();
+                       String bdn = baseDnCmb.getText();
+                       if (EclipseUiUtils.notEmpty(bdn)) {
+                               Dictionary<String, ?> props = UserAdminConf.uriAsProperties(dns.get(bdn));
+                               String dn = LdapAttrs.cn.name() + "=" + cn + "," + UserAdminConf.groupBase.getValue(props) + "," + bdn;
+                               return dn;
+                       }
+                       return null;
+               }
+
+               private void initialiseDnCmb(Combo combo) {
+                       Map<String, String> dns = userAdminWrapper.getKnownBaseDns(true);
+                       if (dns.isEmpty())
+                               throw new CmsException("No writable base dn found. Cannot create group");
+                       combo.setItems(dns.keySet().toArray(new String[0]));
+                       if (dns.size() == 1)
+                               combo.select(0);
+               }
+       }
+
+       private Combo createGridLC(Composite parent, String label) {
+               Label lbl = new Label(parent, SWT.LEAD);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               Combo combo = new Combo(parent, SWT.LEAD | SWT.BORDER | SWT.READ_ONLY);
+               combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               return combo;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/handlers/NewUser.java
new file mode 100644 (file)
index 0000000..e12e6c8
--- /dev/null
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.e4.users.handlers;
+
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.e4.users.UiAdminUtils;
+import org.argeo.cms.e4.users.UserAdminWrapper;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.osgi.useradmin.UserAdminConf;
+import org.eclipse.e4.core.di.annotations.Execute;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Open a wizard that enables creation of a new user. */
+public class NewUser {
+       // private final static Log log = LogFactory.getLog(NewUser.class);
+       // public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".newUser";
+
+       /* DEPENDENCY INJECTION */
+       @Inject
+       private UserAdminWrapper userAdminWrapper;
+
+       @Execute
+       public Object execute() {
+               NewUserWizard newUserWizard = new NewUserWizard();
+               newUserWizard.setWindowTitle("User creation");
+               WizardDialog dialog = new WizardDialog(Display.getCurrent().getActiveShell(), newUserWizard);
+               dialog.open();
+               return null;
+       }
+
+       private class NewUserWizard extends Wizard {
+
+               // pages
+               private MainUserInfoWizardPage mainUserInfo;
+
+               // End user fields
+               private Text dNameTxt, usernameTxt, firstNameTxt, lastNameTxt, primaryMailTxt, pwd1Txt, pwd2Txt;
+               private Combo baseDnCmb;
+
+               public NewUserWizard() {
+
+               }
+
+               @Override
+               public void addPages() {
+                       mainUserInfo = new MainUserInfoWizardPage();
+                       addPage(mainUserInfo);
+                       String message = "Default wizard that also eases user creation tests:\n "
+                                       + "Mail and last name are automatically "
+                                       + "generated form the uid. Password are defauted to 'demo'.";
+                       mainUserInfo.setMessage(message, WizardPage.WARNING);
+               }
+
+               @SuppressWarnings({ "rawtypes", "unchecked" })
+               @Override
+               public boolean performFinish() {
+                       if (!canFinish())
+                               return false;
+                       String username = mainUserInfo.getUsername();
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       try {
+                               User user = (User) userAdminWrapper.getUserAdmin().createRole(getDn(username), Role.USER);
+
+                               Dictionary props = user.getProperties();
+
+                               String lastNameStr = lastNameTxt.getText();
+                               if (EclipseUiUtils.notEmpty(lastNameStr))
+                                       props.put(LdapAttrs.sn.name(), lastNameStr);
+
+                               String firstNameStr = firstNameTxt.getText();
+                               if (EclipseUiUtils.notEmpty(firstNameStr))
+                                       props.put(LdapAttrs.givenName.name(), firstNameStr);
+
+                               String cn = UserAdminUtils.buildDefaultCn(firstNameStr, lastNameStr);
+                               if (EclipseUiUtils.notEmpty(cn))
+                                       props.put(LdapAttrs.cn.name(), cn);
+
+                               String mailStr = primaryMailTxt.getText();
+                               if (EclipseUiUtils.notEmpty(mailStr))
+                                       props.put(LdapAttrs.mail.name(), mailStr);
+
+                               char[] password = mainUserInfo.getPassword();
+                               user.getCredentials().put(null, password);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CREATED, user));
+                               return true;
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot create new user " + username, e);
+                               return false;
+                       }
+               }
+
+               private class MainUserInfoWizardPage extends WizardPage implements ModifyListener, ArgeoNames {
+                       private static final long serialVersionUID = -3150193365151601807L;
+
+                       public MainUserInfoWizardPage() {
+                               super("Main");
+                               setTitle("Required Information");
+                       }
+
+                       @Override
+                       public void createControl(Composite parent) {
+                               Composite composite = new Composite(parent, SWT.NONE);
+                               composite.setLayout(new GridLayout(2, false));
+                               dNameTxt = EclipseUiUtils.createGridLT(composite, "Distinguished name", this);
+                               dNameTxt.setEnabled(false);
+
+                               baseDnCmb = createGridLC(composite, "Base DN");
+                               initialiseDnCmb(baseDnCmb);
+                               baseDnCmb.addModifyListener(this);
+                               baseDnCmb.addModifyListener(new ModifyListener() {
+                                       private static final long serialVersionUID = -1435351236582736843L;
+
+                                       @Override
+                                       public void modifyText(ModifyEvent event) {
+                                               String name = usernameTxt.getText();
+                                               dNameTxt.setText(getDn(name));
+                                       }
+                               });
+
+                               usernameTxt = EclipseUiUtils.createGridLT(composite, "Local ID", this);
+                               usernameTxt.addModifyListener(new ModifyListener() {
+                                       private static final long serialVersionUID = -1435351236582736843L;
+
+                                       @Override
+                                       public void modifyText(ModifyEvent event) {
+                                               String name = usernameTxt.getText();
+                                               if (name.trim().equals("")) {
+                                                       dNameTxt.setText("");
+                                                       lastNameTxt.setText("");
+                                                       primaryMailTxt.setText("");
+                                                       pwd1Txt.setText("");
+                                                       pwd2Txt.setText("");
+                                               } else {
+                                                       dNameTxt.setText(getDn(name));
+                                                       lastNameTxt.setText(name.toUpperCase());
+                                                       primaryMailTxt.setText(getMail(name));
+                                                       pwd1Txt.setText("demo");
+                                                       pwd2Txt.setText("demo");
+                                               }
+                                       }
+                               });
+
+                               primaryMailTxt = EclipseUiUtils.createGridLT(composite, "Email", this);
+                               firstNameTxt = EclipseUiUtils.createGridLT(composite, "First name", this);
+                               lastNameTxt = EclipseUiUtils.createGridLT(composite, "Last name", this);
+                               pwd1Txt = EclipseUiUtils.createGridLP(composite, "Password", this);
+                               pwd2Txt = EclipseUiUtils.createGridLP(composite, "Repeat password", this);
+                               setControl(composite);
+
+                               // Initialize buttons
+                               setPageComplete(false);
+                               getContainer().updateButtons();
+                       }
+
+                       @Override
+                       public void modifyText(ModifyEvent event) {
+                               String message = checkComplete();
+                               if (message != null) {
+                                       setMessage(message, WizardPage.ERROR);
+                                       setPageComplete(false);
+                               } else {
+                                       setMessage("Complete", WizardPage.INFORMATION);
+                                       setPageComplete(true);
+                               }
+                               getContainer().updateButtons();
+                       }
+
+                       /** @return error message or null if complete */
+                       protected String checkComplete() {
+                               String name = usernameTxt.getText();
+
+                               if (name.trim().equals(""))
+                                       return "User name must not be empty";
+                               Role role = userAdminWrapper.getUserAdmin().getRole(getDn(name));
+                               if (role != null)
+                                       return "User " + name + " already exists";
+                               if (!primaryMailTxt.getText().matches(UiAdminUtils.EMAIL_PATTERN))
+                                       return "Not a valid email address";
+                               if (lastNameTxt.getText().trim().equals(""))
+                                       return "Specify a last name";
+                               if (pwd1Txt.getText().trim().equals(""))
+                                       return "Specify a password";
+                               if (pwd2Txt.getText().trim().equals(""))
+                                       return "Repeat the password";
+                               if (!pwd2Txt.getText().equals(pwd1Txt.getText()))
+                                       return "Passwords are different";
+                               return null;
+                       }
+
+                       @Override
+                       public void setVisible(boolean visible) {
+                               super.setVisible(visible);
+                               if (visible)
+                                       if (baseDnCmb.getSelectionIndex() == -1)
+                                               baseDnCmb.setFocus();
+                                       else
+                                               usernameTxt.setFocus();
+                       }
+
+                       public String getUsername() {
+                               return usernameTxt.getText();
+                       }
+
+                       public char[] getPassword() {
+                               return pwd1Txt.getTextChars();
+                       }
+
+               }
+
+               private Map<String, String> getDns() {
+                       return userAdminWrapper.getKnownBaseDns(true);
+               }
+
+               private String getDn(String uid) {
+                       Map<String, String> dns = getDns();
+                       String bdn = baseDnCmb.getText();
+                       if (EclipseUiUtils.notEmpty(bdn)) {
+                               Dictionary<String, ?> props = UserAdminConf.uriAsProperties(dns.get(bdn));
+                               String dn = LdapAttrs.uid.name() + "=" + uid + "," + UserAdminConf.userBase.getValue(props) + "," + bdn;
+                               return dn;
+                       }
+                       return null;
+               }
+
+               private void initialiseDnCmb(Combo combo) {
+                       Map<String, String> dns = userAdminWrapper.getKnownBaseDns(true);
+                       if (dns.isEmpty())
+                               throw new CmsException("No writable base dn found. Cannot create user");
+                       combo.setItems(dns.keySet().toArray(new String[0]));
+                       if (dns.size() == 1)
+                               combo.select(0);
+               }
+
+               private String getMail(String username) {
+                       if (baseDnCmb.getSelectionIndex() == -1)
+                               return null;
+                       String baseDn = baseDnCmb.getText();
+                       try {
+                               LdapName name = new LdapName(baseDn);
+                               List<Rdn> rdns = name.getRdns();
+                               return username + "@" + (String) rdns.get(1).getValue() + '.' + (String) rdns.get(0).getValue();
+                       } catch (InvalidNameException e) {
+                               throw new CmsException("Unable to generate mail for " + username + " with base dn " + baseDn, e);
+                       }
+               }
+       }
+
+       private Combo createGridLC(Composite parent, String label) {
+               Label lbl = new Label(parent, SWT.LEAD);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               Combo combo = new Combo(parent, SWT.LEAD | SWT.BORDER | SWT.READ_ONLY);
+               combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               return combo;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/CommonNameLP.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/CommonNameLP.java
new file mode 100644 (file)
index 0000000..4beacb4
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.naming.LdapAttrs;
+import org.osgi.service.useradmin.User;
+
+/** Simply declare a label provider that returns the common name of a user */
+public class CommonNameLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 5256703081044911941L;
+
+       @Override
+       public String getText(User user) {
+               return UserAdminUtils.getProperty(user, LdapAttrs.cn.name());
+       }
+
+       @Override
+       public String getToolTipText(Object element) {
+               return UserAdminUtils.getProperty((User) element, LdapAttrs.DN);
+       }
+
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/DomainNameLP.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/DomainNameLP.java
new file mode 100644 (file)
index 0000000..48c1d22
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.argeo.cms.util.UserAdminUtils;
+import org.osgi.service.useradmin.User;
+
+/** The human friendly domain name for the corresponding user. */
+public class DomainNameLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 5256703081044911941L;
+
+       @Override
+       public String getText(User user) {
+               return UserAdminUtils.getDomainName(user);
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/MailLP.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/MailLP.java
new file mode 100644 (file)
index 0000000..4224474
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.naming.LdapAttrs;
+import org.osgi.service.useradmin.User;
+
+/** Simply declare a label provider that returns the Primary Mail of a user */
+public class MailLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 8329764452141982707L;
+
+       @Override
+       public String getText(User user) {
+               return UserAdminUtils.getProperty(user, LdapAttrs.mail.name());
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/RoleIconLP.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/RoleIconLP.java
new file mode 100644 (file)
index 0000000..23ee4c1
--- /dev/null
@@ -0,0 +1,35 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.argeo.cms.e4.users.SecurityAdminImages;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeInstance;
+import org.eclipse.swt.graphics.Image;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/** Provide a bundle specific image depending on the current user type */
+public class RoleIconLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 6550449442061090388L;
+
+       @Override
+       public String getText(User user) {
+               return "";
+       }
+
+       @Override
+       public Image getImage(Object element) {
+               User user = (User) element;
+               String dn = user.getName();
+               if (dn.endsWith(NodeConstants.ROLES_BASEDN))
+                       return SecurityAdminImages.ICON_ROLE;
+               else if (user.getType() == Role.GROUP) {
+                       String businessCategory = UserAdminUtils.getProperty(user, LdapAttrs.businessCategory);
+                       if (businessCategory != null && businessCategory.equals(NodeInstance.WORKGROUP))
+                               return SecurityAdminImages.ICON_WORKGROUP;
+                       return SecurityAdminImages.ICON_GROUP;
+               } else
+                       return SecurityAdminImages.ICON_USER;
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserAdminAbstractLP.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserAdminAbstractLP.java
new file mode 100644 (file)
index 0000000..2b90c95
--- /dev/null
@@ -0,0 +1,66 @@
+package org.argeo.cms.e4.users.providers;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.util.UserAdminUtils;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Utility class that add font modifications to a column label provider
+ * depending on the given user properties
+ */
+public abstract class UserAdminAbstractLP extends ColumnLabelProvider {
+       private static final long serialVersionUID = 137336765024922368L;
+
+       // private Font italic;
+       private Font bold;
+
+       @Override
+       public Font getFont(Object element) {
+               // Self as bold
+               try {
+                       LdapName selfUserName = UserAdminUtils.getCurrentUserLdapName();
+                       String userName = ((User) element).getName();
+                       LdapName userLdapName = new LdapName(userName);
+                       if (userLdapName.equals(selfUserName)) {
+                               if (bold == null)
+                                       bold = JFaceResources.getFontRegistry()
+                                                       .defaultFontDescriptor().setStyle(SWT.BOLD)
+                                                       .createFont(Display.getCurrent());
+                               return bold;
+                       }
+               } catch (InvalidNameException e) {
+                       throw new CmsException("cannot parse dn for " + element, e);
+               }
+
+               // Disabled as Italic
+               // Node userProfile = (Node) elem;
+               // if (!userProfile.getProperty(ARGEO_ENABLED).getBoolean())
+               // return italic;
+
+               return null;
+               // return super.getFont(element);
+       }
+
+       @Override
+       public String getText(Object element) {
+               User user = (User) element;
+               return getText(user);
+       }
+
+       public void setDisplay(Display display) {
+               // italic = JFaceResources.getFontRegistry().defaultFontDescriptor()
+               // .setStyle(SWT.ITALIC).createFont(display);
+               bold = JFaceResources.getFontRegistry().defaultFontDescriptor()
+                               .setStyle(SWT.BOLD).createFont(Display.getCurrent());
+       }
+
+       public abstract String getText(User user);
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserDragListener.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserDragListener.java
new file mode 100644 (file)
index 0000000..56a2624
--- /dev/null
@@ -0,0 +1,40 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.osgi.service.useradmin.User;
+
+/** Default drag listener to modify group and users via the UI */
+public class UserDragListener implements DragSourceListener {
+       private static final long serialVersionUID = -2074337775033781454L;
+       private final Viewer viewer;
+
+       public UserDragListener(Viewer viewer) {
+               this.viewer = viewer;
+       }
+
+       public void dragStart(DragSourceEvent event) {
+               // TODO implement finer checks
+               IStructuredSelection selection = (IStructuredSelection) viewer
+                               .getSelection();
+               if (selection.isEmpty() || selection.size() > 1)
+                       event.doit = false;
+               else
+                       event.doit = true;
+       }
+
+       public void dragSetData(DragSourceEvent event) {
+               // TODO Support multiple selection
+               Object obj = ((IStructuredSelection) viewer.getSelection())
+                               .getFirstElement();
+               if (obj != null) {
+                       User user = (User) obj;
+                       event.data = user.getName();
+               }
+       }
+
+       public void dragFinished(DragSourceEvent event) {
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserFilter.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserFilter.java
new file mode 100644 (file)
index 0000000..3ad1c53
--- /dev/null
@@ -0,0 +1,58 @@
+package org.argeo.cms.e4.users.providers;
+
+import static org.argeo.eclipse.ui.EclipseUiUtils.notEmpty;
+
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Filter user list using JFace mechanism on the client (yet on the server) side
+ * rather than having the UserAdmin to process the search
+ */
+public class UserFilter extends ViewerFilter {
+       private static final long serialVersionUID = 5082509381672880568L;
+
+       private String searchString;
+       private boolean showSystemRole = true;
+
+       private final String[] knownProps = { LdapAttrs.DN, LdapAttrs.cn.name(), LdapAttrs.givenName.name(),
+                       LdapAttrs.sn.name(), LdapAttrs.uid.name(), LdapAttrs.description.name(), LdapAttrs.mail.name() };
+
+       public void setSearchText(String s) {
+               // ensure that the value can be used for matching
+               if (notEmpty(s))
+                       searchString = ".*" + s.toLowerCase() + ".*";
+               else
+                       searchString = ".*";
+       }
+
+       public void setShowSystemRole(boolean showSystemRole) {
+               this.showSystemRole = showSystemRole;
+       }
+
+       @Override
+       public boolean select(Viewer viewer, Object parentElement, Object element) {
+               User user = (User) element;
+               if (!showSystemRole && user.getName().matches(".*(" + NodeConstants.ROLES_BASEDN + ")"))
+                       // UserAdminUtils.getProperty(user, LdifName.dn.name())
+                       // .toLowerCase().endsWith(AuthConstants.ROLES_BASEDN))
+                       return false;
+
+               if (searchString == null || searchString.length() == 0)
+                       return true;
+
+               if (user.getName().matches(searchString))
+                       return true;
+
+               for (String key : knownProps) {
+                       String currVal = UserAdminUtils.getProperty(user, key);
+                       if (notEmpty(currVal) && currVal.toLowerCase().matches(searchString))
+                               return true;
+               }
+               return false;
+       }
+}
diff --git a/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserNameLP.java b/org.argeo.cms.e4/src/org/argeo/cms/e4/users/providers/UserNameLP.java
new file mode 100644 (file)
index 0000000..3cd00eb
--- /dev/null
@@ -0,0 +1,13 @@
+package org.argeo.cms.e4.users.providers;
+
+import org.osgi.service.useradmin.User;
+
+/** Simply declare a label provider that returns the username of a user */
+public class UserNameLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 6550449442061090388L;
+
+       @Override
+       public String getText(User user) {
+               return user.getName();
+       }
+}
diff --git a/org.argeo.cms.ui.theme/.classpath b/org.argeo.cms.ui.theme/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms.ui.theme/.gitignore b/org.argeo.cms.ui.theme/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.cms.ui.theme/.project b/org.argeo.cms.ui.theme/.project
new file mode 100644 (file)
index 0000000..5f90021
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.ui.theme</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.cms.ui.theme/.settings/org.eclipse.jdt.core.prefs b/org.argeo.cms.ui.theme/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..0c68a61
--- /dev/null
@@ -0,0 +1,7 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/org.argeo.cms.ui.theme/.settings/org.eclipse.pde.core.prefs b/org.argeo.cms.ui.theme/.settings/org.eclipse.pde.core.prefs
new file mode 100644 (file)
index 0000000..f29e940
--- /dev/null
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+pluginProject.extensions=false
+resolve.requirebundle=false
diff --git a/org.argeo.cms.ui.theme/META-INF/.gitignore b/org.argeo.cms.ui.theme/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.cms.ui.theme/bnd.bnd b/org.argeo.cms.ui.theme/bnd.bnd
new file mode 100644 (file)
index 0000000..2b2a02f
--- /dev/null
@@ -0,0 +1 @@
+Bundle-ActivationPolicy: lazy
diff --git a/org.argeo.cms.ui.theme/build.properties b/org.argeo.cms.ui.theme/build.properties
new file mode 100644 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
diff --git a/org.argeo.cms.ui.theme/icons/active.gif b/org.argeo.cms.ui.theme/icons/active.gif
new file mode 100644 (file)
index 0000000..7d24707
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/active.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/add.gif b/org.argeo.cms.ui.theme/icons/add.gif
new file mode 100644 (file)
index 0000000..252d7eb
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/add.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/add.png b/org.argeo.cms.ui.theme/icons/add.png
new file mode 100644 (file)
index 0000000..c7edfec
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/add.png differ
diff --git a/org.argeo.cms.ui.theme/icons/addFolder.gif b/org.argeo.cms.ui.theme/icons/addFolder.gif
new file mode 100644 (file)
index 0000000..d3f43d9
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/addFolder.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/addPrivileges.gif b/org.argeo.cms.ui.theme/icons/addPrivileges.gif
new file mode 100644 (file)
index 0000000..a6b251f
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/addPrivileges.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/addRepo.gif b/org.argeo.cms.ui.theme/icons/addRepo.gif
new file mode 100644 (file)
index 0000000..26d81c0
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/addRepo.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/addWorkspace.png b/org.argeo.cms.ui.theme/icons/addWorkspace.png
new file mode 100644 (file)
index 0000000..bbee775
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/addWorkspace.png differ
diff --git a/org.argeo.cms.ui.theme/icons/adminLog.gif b/org.argeo.cms.ui.theme/icons/adminLog.gif
new file mode 100644 (file)
index 0000000..6ef3bca
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/adminLog.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/batch.gif b/org.argeo.cms.ui.theme/icons/batch.gif
new file mode 100644 (file)
index 0000000..b8ca14a
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/batch.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/begin.gif b/org.argeo.cms.ui.theme/icons/begin.gif
new file mode 100755 (executable)
index 0000000..feb8e94
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/begin.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/binary.png b/org.argeo.cms.ui.theme/icons/binary.png
new file mode 100644 (file)
index 0000000..fdf4f82
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/binary.png differ
diff --git a/org.argeo.cms.ui.theme/icons/browser.gif b/org.argeo.cms.ui.theme/icons/browser.gif
new file mode 100644 (file)
index 0000000..6c7320c
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/browser.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/bundles.gif b/org.argeo.cms.ui.theme/icons/bundles.gif
new file mode 100644 (file)
index 0000000..e9a6bd9
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/bundles.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/changePassword.gif b/org.argeo.cms.ui.theme/icons/changePassword.gif
new file mode 100644 (file)
index 0000000..274a850
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/changePassword.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/clear.gif b/org.argeo.cms.ui.theme/icons/clear.gif
new file mode 100644 (file)
index 0000000..6bc10f9
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/clear.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/close-all.png b/org.argeo.cms.ui.theme/icons/close-all.png
new file mode 100644 (file)
index 0000000..85d4d42
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/close-all.png differ
diff --git a/org.argeo.cms.ui.theme/icons/commit.gif b/org.argeo.cms.ui.theme/icons/commit.gif
new file mode 100755 (executable)
index 0000000..876f3eb
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/commit.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/delete.png b/org.argeo.cms.ui.theme/icons/delete.png
new file mode 100644 (file)
index 0000000..676a39d
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/delete.png differ
diff --git a/org.argeo.cms.ui.theme/icons/dumpNode.gif b/org.argeo.cms.ui.theme/icons/dumpNode.gif
new file mode 100644 (file)
index 0000000..14eb1be
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/dumpNode.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/file.gif b/org.argeo.cms.ui.theme/icons/file.gif
new file mode 100644 (file)
index 0000000..ef30288
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/file.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/folder.gif b/org.argeo.cms.ui.theme/icons/folder.gif
new file mode 100644 (file)
index 0000000..42e027c
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/folder.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/getSize.gif b/org.argeo.cms.ui.theme/icons/getSize.gif
new file mode 100644 (file)
index 0000000..b05bf3e
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/getSize.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/group.png b/org.argeo.cms.ui.theme/icons/group.png
new file mode 100644 (file)
index 0000000..cc6683a
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/group.png differ
diff --git a/org.argeo.cms.ui.theme/icons/home.gif b/org.argeo.cms.ui.theme/icons/home.gif
new file mode 100644 (file)
index 0000000..fd0c669
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/home.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/home.png b/org.argeo.cms.ui.theme/icons/home.png
new file mode 100644 (file)
index 0000000..5eb0967
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/home.png differ
diff --git a/org.argeo.cms.ui.theme/icons/import_fs.png b/org.argeo.cms.ui.theme/icons/import_fs.png
new file mode 100644 (file)
index 0000000..d7c890c
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/import_fs.png differ
diff --git a/org.argeo.cms.ui.theme/icons/installed.gif b/org.argeo.cms.ui.theme/icons/installed.gif
new file mode 100644 (file)
index 0000000..2988716
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/installed.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/log.gif b/org.argeo.cms.ui.theme/icons/log.gif
new file mode 100644 (file)
index 0000000..e3ecc55
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/log.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/logout.png b/org.argeo.cms.ui.theme/icons/logout.png
new file mode 100644 (file)
index 0000000..f2952fa
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/logout.png differ
diff --git a/org.argeo.cms.ui.theme/icons/maintenance.gif b/org.argeo.cms.ui.theme/icons/maintenance.gif
new file mode 100644 (file)
index 0000000..e5690ec
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/maintenance.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/node.gif b/org.argeo.cms.ui.theme/icons/node.gif
new file mode 100644 (file)
index 0000000..364c0e7
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/node.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/nodes.gif b/org.argeo.cms.ui.theme/icons/nodes.gif
new file mode 100644 (file)
index 0000000..bba3dbc
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/nodes.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/osgi_explorer.gif b/org.argeo.cms.ui.theme/icons/osgi_explorer.gif
new file mode 100644 (file)
index 0000000..e9a6bd9
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/osgi_explorer.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/password.gif b/org.argeo.cms.ui.theme/icons/password.gif
new file mode 100644 (file)
index 0000000..a6b251f
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/password.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/person-logged-in.png b/org.argeo.cms.ui.theme/icons/person-logged-in.png
new file mode 100644 (file)
index 0000000..87acc14
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/person-logged-in.png differ
diff --git a/org.argeo.cms.ui.theme/icons/person.png b/org.argeo.cms.ui.theme/icons/person.png
new file mode 100644 (file)
index 0000000..7d979a5
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/person.png differ
diff --git a/org.argeo.cms.ui.theme/icons/query.png b/org.argeo.cms.ui.theme/icons/query.png
new file mode 100644 (file)
index 0000000..54c089d
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/query.png differ
diff --git a/org.argeo.cms.ui.theme/icons/refresh.png b/org.argeo.cms.ui.theme/icons/refresh.png
new file mode 100644 (file)
index 0000000..71b3481
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/refresh.png differ
diff --git a/org.argeo.cms.ui.theme/icons/remote_connected.gif b/org.argeo.cms.ui.theme/icons/remote_connected.gif
new file mode 100644 (file)
index 0000000..1492b4e
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/remote_connected.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/remote_disconnected.gif b/org.argeo.cms.ui.theme/icons/remote_disconnected.gif
new file mode 100644 (file)
index 0000000..6c54da9
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/remote_disconnected.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/remove.gif b/org.argeo.cms.ui.theme/icons/remove.gif
new file mode 100644 (file)
index 0000000..0ae6dec
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/remove.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/removePrivileges.gif b/org.argeo.cms.ui.theme/icons/removePrivileges.gif
new file mode 100644 (file)
index 0000000..aa78fd2
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/removePrivileges.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/rename.gif b/org.argeo.cms.ui.theme/icons/rename.gif
new file mode 100644 (file)
index 0000000..8048405
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/rename.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/repositories.gif b/org.argeo.cms.ui.theme/icons/repositories.gif
new file mode 100644 (file)
index 0000000..c13bea1
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/repositories.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/repository_connected.gif b/org.argeo.cms.ui.theme/icons/repository_connected.gif
new file mode 100644 (file)
index 0000000..a15fa55
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/repository_connected.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/repository_disconnected.gif b/org.argeo.cms.ui.theme/icons/repository_disconnected.gif
new file mode 100644 (file)
index 0000000..4576dc5
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/repository_disconnected.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/resolved.gif b/org.argeo.cms.ui.theme/icons/resolved.gif
new file mode 100644 (file)
index 0000000..f4a1ea1
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/resolved.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/role.gif b/org.argeo.cms.ui.theme/icons/role.gif
new file mode 100644 (file)
index 0000000..274a850
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/role.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/rollback.gif b/org.argeo.cms.ui.theme/icons/rollback.gif
new file mode 100755 (executable)
index 0000000..c753995
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/rollback.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/save-all.png b/org.argeo.cms.ui.theme/icons/save-all.png
new file mode 100644 (file)
index 0000000..b68a29b
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/save-all.png differ
diff --git a/org.argeo.cms.ui.theme/icons/save.gif b/org.argeo.cms.ui.theme/icons/save.gif
new file mode 100644 (file)
index 0000000..654ad7b
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/save.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/save.png b/org.argeo.cms.ui.theme/icons/save.png
new file mode 100644 (file)
index 0000000..f27ef2d
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/save.png differ
diff --git a/org.argeo.cms.ui.theme/icons/save_security.png b/org.argeo.cms.ui.theme/icons/save_security.png
new file mode 100644 (file)
index 0000000..ca41dc9
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/save_security.png differ
diff --git a/org.argeo.cms.ui.theme/icons/save_security_disabled.png b/org.argeo.cms.ui.theme/icons/save_security_disabled.png
new file mode 100644 (file)
index 0000000..fb7d08d
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/save_security_disabled.png differ
diff --git a/org.argeo.cms.ui.theme/icons/security.gif b/org.argeo.cms.ui.theme/icons/security.gif
new file mode 100644 (file)
index 0000000..57fb95e
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/security.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/service_published.gif b/org.argeo.cms.ui.theme/icons/service_published.gif
new file mode 100644 (file)
index 0000000..17f771a
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/service_published.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/service_referenced.gif b/org.argeo.cms.ui.theme/icons/service_referenced.gif
new file mode 100644 (file)
index 0000000..c24a95f
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/service_referenced.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/sort.gif b/org.argeo.cms.ui.theme/icons/sort.gif
new file mode 100644 (file)
index 0000000..23c5d0b
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/sort.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/starting.gif b/org.argeo.cms.ui.theme/icons/starting.gif
new file mode 100644 (file)
index 0000000..563743d
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/starting.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/sync.gif b/org.argeo.cms.ui.theme/icons/sync.gif
new file mode 100644 (file)
index 0000000..b4fa052
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/sync.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/user.gif b/org.argeo.cms.ui.theme/icons/user.gif
new file mode 100644 (file)
index 0000000..90a0014
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/user.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/users.gif b/org.argeo.cms.ui.theme/icons/users.gif
new file mode 100644 (file)
index 0000000..2de7edd
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/users.gif differ
diff --git a/org.argeo.cms.ui.theme/icons/workgroup.png b/org.argeo.cms.ui.theme/icons/workgroup.png
new file mode 100644 (file)
index 0000000..7fef996
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/workgroup.png differ
diff --git a/org.argeo.cms.ui.theme/icons/workgroup.xcf b/org.argeo.cms.ui.theme/icons/workgroup.xcf
new file mode 100644 (file)
index 0000000..f517c82
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/workgroup.xcf differ
diff --git a/org.argeo.cms.ui.theme/icons/workspace_connected.png b/org.argeo.cms.ui.theme/icons/workspace_connected.png
new file mode 100644 (file)
index 0000000..0430baa
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/workspace_connected.png differ
diff --git a/org.argeo.cms.ui.theme/icons/workspace_disconnected.png b/org.argeo.cms.ui.theme/icons/workspace_disconnected.png
new file mode 100644 (file)
index 0000000..fddcb8c
Binary files /dev/null and b/org.argeo.cms.ui.theme/icons/workspace_disconnected.png differ
diff --git a/org.argeo.cms.ui.theme/pom.xml b/org.argeo.cms.ui.theme/pom.xml
new file mode 100644 (file)
index 0000000..36118d0
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.cms.ui.theme</artifactId>
+       <name>CMS UI Theme</name>
+       <packaging>jar</packaging>
+       <dependencies>
+               <!-- UI -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.rwt</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.jface</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+               
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.cms.ui.theme/rap/argeo-studio.css b/org.argeo.cms.ui.theme/rap/argeo-studio.css
new file mode 100644 (file)
index 0000000..2d75555
--- /dev/null
@@ -0,0 +1,21 @@
+Composite.qa {
+       background-color: gray;
+       color: white;
+}
+
+Button[PUSH].qa {
+       color: white;
+       background-color: gray;
+       padding: 0px;
+       spacing: 0px;
+       border: none;
+}
+
+Composite.support {
+       background-color: red;
+}
+
+Label.support {
+       background-color: red;
+       color: white;
+}
diff --git a/org.argeo.cms.ui.theme/src/org/argeo/cms/ui/theme/CmsImages.java b/org.argeo.cms.ui.theme/src/org/argeo/cms/ui/theme/CmsImages.java
new file mode 100644 (file)
index 0000000..1c4d79e
--- /dev/null
@@ -0,0 +1,49 @@
+package org.argeo.cms.ui.theme;
+
+import java.net.URL;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+public class CmsImages {
+       private static BundleContext themeBc = FrameworkUtil.getBundle(CmsImages.class).getBundleContext();
+
+       final public static String ICONS_BASE = "icons/";
+       final public static String TYPES_BASE = ICONS_BASE + "types/";
+       final public static String ACTIONS_BASE = ICONS_BASE + "actions/";
+
+       public static Image createIcon(String name) {
+               return createImg(CmsImages.ICONS_BASE + name);
+       }
+
+       public static Image createAction(String name) {
+               return createImg(CmsImages.ACTIONS_BASE + name);
+       }
+
+       public static Image createType(String name) {
+               return createImg(CmsImages.TYPES_BASE + name);
+       }
+
+       public static Image createImg(String name) {
+               return CmsImages.createDesc(name).createImage(Display.getDefault());
+       }
+
+       public static ImageDescriptor createDesc(String name) {
+               return createDesc(themeBc, name);
+       }
+
+       public static ImageDescriptor createDesc(BundleContext bc, String name) {
+               URL url = bc.getBundle().getResource(name);
+               if (url == null)
+                       return ImageDescriptor.getMissingImageDescriptor();
+               return ImageDescriptor.createFromURL(url);
+       }
+
+       public static Image createImg(BundleContext bc, String name) {
+               return createDesc(bc, name).createImage(Display.getDefault());
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench.rap/.classpath b/org.argeo.cms.ui.workbench.rap/.classpath
new file mode 100644 (file)
index 0000000..457b115
--- /dev/null
@@ -0,0 +1,9 @@
+<?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-1.8" />
+       <classpathentry kind="output" path="bin" />
+</classpath>
diff --git a/org.argeo.cms.ui.workbench.rap/.gitignore b/org.argeo.cms.ui.workbench.rap/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.cms.ui.workbench.rap/.project b/org.argeo.cms.ui.workbench.rap/.project
new file mode 100644 (file)
index 0000000..49b4b90
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.ui.workbench.rap</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.cms.ui.workbench.rap/META-INF/.gitignore b/org.argeo.cms.ui.workbench.rap/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.cms.ui.workbench.rap/META-INF/spring/commands.xml b/org.argeo.cms.ui.workbench.rap/META-INF/spring/commands.xml
new file mode 100644 (file)
index 0000000..2bfa179
--- /dev/null
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd">
+
+       <bean id="org.argeo.cms.ui.workbench.rap.openChangePasswordDialog"
+               class="org.argeo.cms.ui.workbench.commands.OpenChangePasswordDialog"
+               scope="prototype">
+               <property name="userAdmin" ref="userAdmin" />
+               <property name="userTransaction" ref="userTransaction" />
+               <property name="keyring" ref="keyring" />
+       </bean>
+
+       <!-- RAP Specific command and corresponding service to enable open file -->
+       <bean id="org.argeo.cms.ui.workbench.openFile" class="org.argeo.eclipse.ui.specific.OpenFile"
+               scope="prototype">
+               <property name="openFileServiceId"
+                       value="org.argeo.security.ui.specific.openFileService" />
+       </bean>
+</beans>
diff --git a/org.argeo.cms.ui.workbench.rap/META-INF/spring/osgi.xml b/org.argeo.cms.ui.workbench.rap/META-INF/spring/osgi.xml
new file mode 100644 (file)
index 0000000..231b7d8
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<beans:beans xmlns="http://www.springframework.org/schema/osgi"\r
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"\r
+       xmlns:osgi="http://www.springframework.org/schema/osgi"\r
+       xsi:schemaLocation="http://www.springframework.org/schema/osgi  \r
+       http://www.springframework.org/schema/osgi/spring-osgi-1.1.xsd\r
+       http://www.springframework.org/schema/beans   \r
+       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"\r
+       osgi:default-timeout="30000">\r
+\r
+       <reference id="userAdmin" interface="org.osgi.service.useradmin.UserAdmin" />\r
+       <reference id="userTransaction" interface="javax.transaction.UserTransaction" />\r
+       <reference id="keyring" interface="org.argeo.node.security.CryptoKeyring" />\r
+</beans:beans>\r
diff --git a/org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle.properties b/org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle.properties
new file mode 100644 (file)
index 0000000..4dff7af
--- /dev/null
@@ -0,0 +1 @@
+changePassword=Change password
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle_de.properties b/org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle_de.properties
new file mode 100644 (file)
index 0000000..3769714
--- /dev/null
@@ -0,0 +1 @@
+changePassword=Passwort ändern
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle_fr.properties b/org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle_fr.properties
new file mode 100644 (file)
index 0000000..158d6fa
--- /dev/null
@@ -0,0 +1 @@
+changePassword=Changer de mot de passe
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle_ru.properties b/org.argeo.cms.ui.workbench.rap/OSGI-INF/l10n/bundle_ru.properties
new file mode 100644 (file)
index 0000000..11dd100
--- /dev/null
@@ -0,0 +1 @@
+changePassword=\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C
diff --git a/org.argeo.cms.ui.workbench.rap/bnd.bnd b/org.argeo.cms.ui.workbench.rap/bnd.bnd
new file mode 100644 (file)
index 0000000..f9e7f66
--- /dev/null
@@ -0,0 +1,14 @@
+Bundle-SymbolicName: org.argeo.cms.ui.workbench.rap;singleton:=true
+Bundle-Activator: org.argeo.cms.ui.workbench.rap.SecureRapActivator
+Bundle-ActivationPolicy: lazy
+
+Require-Bundle: org.eclipse.rap.ui,org.eclipse.core.runtime
+
+Import-Package: org.argeo.cms,\
+org.argeo.cms.auth,\
+org.argeo.cms.ui.workbench,\
+org.argeo.cms.ui.workbench.commands,\
+org.argeo.eclipse.ui.specific,\
+org.argeo.eclipse.spring,\
+org.argeo.node,\
+*
diff --git a/org.argeo.cms.ui.workbench.rap/branding/afterLogout.html b/org.argeo.cms.ui.workbench.rap/branding/afterLogout.html
new file mode 100644 (file)
index 0000000..ae0901b
--- /dev/null
@@ -0,0 +1,18 @@
+<html>
+<head></head>
+<body>
+<center>
+<table height="100%">
+<tr>
+       <td style="vertical-align:middle">
+               <a 
+                       style="font-family:sans-serif;color:#0066CC;text-decoration:none;" 
+                       href="node" 
+                       title="Click to log in"
+               >Login...</a>
+       </td>
+</tr>
+</table>
+</center>
+</body>
+</html>
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench.rap/branding/empty.html b/org.argeo.cms.ui.workbench.rap/branding/empty.html
new file mode 100644 (file)
index 0000000..94fe28a
--- /dev/null
@@ -0,0 +1,5 @@
+<html>
+<head></head>
+<body>
+</body>
+</html>
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench.rap/branding/favicon.ico b/org.argeo.cms.ui.workbench.rap/branding/favicon.ico
new file mode 100644 (file)
index 0000000..213cdf7
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/branding/favicon.ico differ
diff --git a/org.argeo.cms.ui.workbench.rap/branding/login.html b/org.argeo.cms.ui.workbench.rap/branding/login.html
new file mode 100644 (file)
index 0000000..6de7eb2
--- /dev/null
@@ -0,0 +1,18 @@
+<html>
+<head></head>
+<body>
+<center>
+<table height="100%">
+<tr>
+       <td style="vertical-align:middle">
+               <a 
+                       style="font-family:sans-serif;color:#0066CC;text-decoration:none;" 
+                       href="javascript:location.reload(true);" 
+                       title="Click to log in"
+               >Login...</a>
+       </td>
+</tr>
+</table>
+</center>
+</body>
+</html>
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench.rap/branding/public.html b/org.argeo.cms.ui.workbench.rap/branding/public.html
new file mode 100644 (file)
index 0000000..e50f6e9
--- /dev/null
@@ -0,0 +1,18 @@
+<html>
+<head></head>
+<body>
+<center>
+<table height="100%">
+<tr>
+       <td style="vertical-align:middle">
+               <a 
+                       style="font-family:sans-serif;color:#0066CC;text-decoration:none;" 
+                       href="javascript:location.reload(true);" 
+                       title="Refresh"
+               >Refresh...</a>
+       </td>
+</tr>
+</table>
+</center>
+</body>
+</html>
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench.rap/build.properties b/org.argeo.cms.ui.workbench.rap/build.properties
new file mode 100644 (file)
index 0000000..ae37429
--- /dev/null
@@ -0,0 +1,2 @@
+source.. = src/
+bin.includes = OSGI-INF/
diff --git a/org.argeo.cms.ui.workbench.rap/icons/active.gif b/org.argeo.cms.ui.workbench.rap/icons/active.gif
new file mode 100644 (file)
index 0000000..7d24707
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/active.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/add.gif b/org.argeo.cms.ui.workbench.rap/icons/add.gif
new file mode 100644 (file)
index 0000000..252d7eb
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/add.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/addFolder.gif b/org.argeo.cms.ui.workbench.rap/icons/addFolder.gif
new file mode 100644 (file)
index 0000000..d3f43d9
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/addFolder.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/addPrivileges.gif b/org.argeo.cms.ui.workbench.rap/icons/addPrivileges.gif
new file mode 100644 (file)
index 0000000..a6b251f
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/addPrivileges.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/addRepo.gif b/org.argeo.cms.ui.workbench.rap/icons/addRepo.gif
new file mode 100644 (file)
index 0000000..26d81c0
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/addRepo.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/addWorkspace.png b/org.argeo.cms.ui.workbench.rap/icons/addWorkspace.png
new file mode 100644 (file)
index 0000000..bbee775
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/addWorkspace.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/binary.png b/org.argeo.cms.ui.workbench.rap/icons/binary.png
new file mode 100644 (file)
index 0000000..fdf4f82
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/binary.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/browser.gif b/org.argeo.cms.ui.workbench.rap/icons/browser.gif
new file mode 100644 (file)
index 0000000..6c7320c
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/browser.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/bundles.gif b/org.argeo.cms.ui.workbench.rap/icons/bundles.gif
new file mode 100644 (file)
index 0000000..e9a6bd9
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/bundles.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/close-all.png b/org.argeo.cms.ui.workbench.rap/icons/close-all.png
new file mode 100644 (file)
index 0000000..85d4d42
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/close-all.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/closeAll.gif b/org.argeo.cms.ui.workbench.rap/icons/closeAll.gif
new file mode 100644 (file)
index 0000000..28a3785
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/closeAll.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/dumpNode.gif b/org.argeo.cms.ui.workbench.rap/icons/dumpNode.gif
new file mode 100644 (file)
index 0000000..14eb1be
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/dumpNode.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/exit.png b/org.argeo.cms.ui.workbench.rap/icons/exit.png
new file mode 100644 (file)
index 0000000..cfbf9d1
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/exit.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/file.gif b/org.argeo.cms.ui.workbench.rap/icons/file.gif
new file mode 100644 (file)
index 0000000..ef30288
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/file.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/folder.gif b/org.argeo.cms.ui.workbench.rap/icons/folder.gif
new file mode 100644 (file)
index 0000000..42e027c
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/folder.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/getSize.gif b/org.argeo.cms.ui.workbench.rap/icons/getSize.gif
new file mode 100644 (file)
index 0000000..b05bf3e
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/getSize.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/home.gif b/org.argeo.cms.ui.workbench.rap/icons/home.gif
new file mode 100644 (file)
index 0000000..fd0c669
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/home.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/home.png b/org.argeo.cms.ui.workbench.rap/icons/home.png
new file mode 100644 (file)
index 0000000..5eb0967
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/home.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/import_fs.png b/org.argeo.cms.ui.workbench.rap/icons/import_fs.png
new file mode 100644 (file)
index 0000000..d7c890c
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/import_fs.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/installed.gif b/org.argeo.cms.ui.workbench.rap/icons/installed.gif
new file mode 100644 (file)
index 0000000..2988716
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/installed.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/main.gif b/org.argeo.cms.ui.workbench.rap/icons/main.gif
new file mode 100644 (file)
index 0000000..90a0014
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/main.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/node.gif b/org.argeo.cms.ui.workbench.rap/icons/node.gif
new file mode 100644 (file)
index 0000000..364c0e7
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/node.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/nodes.gif b/org.argeo.cms.ui.workbench.rap/icons/nodes.gif
new file mode 100644 (file)
index 0000000..bba3dbc
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/nodes.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/osgi_explorer.gif b/org.argeo.cms.ui.workbench.rap/icons/osgi_explorer.gif
new file mode 100644 (file)
index 0000000..e9a6bd9
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/osgi_explorer.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/password.gif b/org.argeo.cms.ui.workbench.rap/icons/password.gif
new file mode 100644 (file)
index 0000000..a6b251f
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/password.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/person-logged-in.png b/org.argeo.cms.ui.workbench.rap/icons/person-logged-in.png
new file mode 100644 (file)
index 0000000..87acc14
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/person-logged-in.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/preferences.png b/org.argeo.cms.ui.workbench.rap/icons/preferences.png
new file mode 100644 (file)
index 0000000..aa0dc0b
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/preferences.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/query.png b/org.argeo.cms.ui.workbench.rap/icons/query.png
new file mode 100644 (file)
index 0000000..54c089d
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/query.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/refresh.png b/org.argeo.cms.ui.workbench.rap/icons/refresh.png
new file mode 100644 (file)
index 0000000..a3884fb
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/refresh.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/remote_connected.gif b/org.argeo.cms.ui.workbench.rap/icons/remote_connected.gif
new file mode 100644 (file)
index 0000000..1492b4e
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/remote_connected.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/remote_disconnected.gif b/org.argeo.cms.ui.workbench.rap/icons/remote_disconnected.gif
new file mode 100644 (file)
index 0000000..6c54da9
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/remote_disconnected.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/remove.gif b/org.argeo.cms.ui.workbench.rap/icons/remove.gif
new file mode 100644 (file)
index 0000000..0ae6dec
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/remove.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/removePrivileges.gif b/org.argeo.cms.ui.workbench.rap/icons/removePrivileges.gif
new file mode 100644 (file)
index 0000000..aa78fd2
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/removePrivileges.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/rename.gif b/org.argeo.cms.ui.workbench.rap/icons/rename.gif
new file mode 100644 (file)
index 0000000..8048405
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/rename.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/repositories.gif b/org.argeo.cms.ui.workbench.rap/icons/repositories.gif
new file mode 100644 (file)
index 0000000..c13bea1
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/repositories.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/repository_connected.gif b/org.argeo.cms.ui.workbench.rap/icons/repository_connected.gif
new file mode 100644 (file)
index 0000000..a15fa55
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/repository_connected.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/repository_disconnected.gif b/org.argeo.cms.ui.workbench.rap/icons/repository_disconnected.gif
new file mode 100644 (file)
index 0000000..4576dc5
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/repository_disconnected.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/resolved.gif b/org.argeo.cms.ui.workbench.rap/icons/resolved.gif
new file mode 100644 (file)
index 0000000..f4a1ea1
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/resolved.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/role.gif b/org.argeo.cms.ui.workbench.rap/icons/role.gif
new file mode 100644 (file)
index 0000000..274a850
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/role.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/save-all.png b/org.argeo.cms.ui.workbench.rap/icons/save-all.png
new file mode 100644 (file)
index 0000000..b68a29b
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/save-all.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/save.png b/org.argeo.cms.ui.workbench.rap/icons/save.png
new file mode 100644 (file)
index 0000000..f27ef2d
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/save.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/security.gif b/org.argeo.cms.ui.workbench.rap/icons/security.gif
new file mode 100644 (file)
index 0000000..57fb95e
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/security.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/service_published.gif b/org.argeo.cms.ui.workbench.rap/icons/service_published.gif
new file mode 100644 (file)
index 0000000..17f771a
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/service_published.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/service_referenced.gif b/org.argeo.cms.ui.workbench.rap/icons/service_referenced.gif
new file mode 100644 (file)
index 0000000..c24a95f
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/service_referenced.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/sort.gif b/org.argeo.cms.ui.workbench.rap/icons/sort.gif
new file mode 100644 (file)
index 0000000..23c5d0b
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/sort.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/starting.gif b/org.argeo.cms.ui.workbench.rap/icons/starting.gif
new file mode 100644 (file)
index 0000000..563743d
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/starting.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/user.gif b/org.argeo.cms.ui.workbench.rap/icons/user.gif
new file mode 100644 (file)
index 0000000..90a0014
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/user.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/users.gif b/org.argeo.cms.ui.workbench.rap/icons/users.gif
new file mode 100644 (file)
index 0000000..2de7edd
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/users.gif differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/workspace_connected.png b/org.argeo.cms.ui.workbench.rap/icons/workspace_connected.png
new file mode 100644 (file)
index 0000000..0430baa
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/workspace_connected.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/icons/workspace_disconnected.png b/org.argeo.cms.ui.workbench.rap/icons/workspace_disconnected.png
new file mode 100644 (file)
index 0000000..fddcb8c
Binary files /dev/null and b/org.argeo.cms.ui.workbench.rap/icons/workspace_disconnected.png differ
diff --git a/org.argeo.cms.ui.workbench.rap/plugin.xml b/org.argeo.cms.ui.workbench.rap/plugin.xml
new file mode 100644 (file)
index 0000000..3dfbf2d
--- /dev/null
@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?eclipse version="3.4"?>
+<plugin>
+   <extension
+         point="org.eclipse.rap.ui.entrypoint">
+      <entrypoint
+            id="org.argeo.cms.ui.workbench.rap.secureEntryPoint"
+            class="org.argeo.cms.ui.workbench.rap.RapWorkbenchLogin"
+            path="/node"
+            brandingId="org.argeo.cms.ui.workbench.rap.defaultBranding">
+      </entrypoint>
+      <entrypoint
+            id="org.argeo.cms.ui.workbench.rap.anonymousEntryPoint"
+            class="org.argeo.cms.ui.workbench.rap.AnonymousEntryPoint"
+            path="/public"
+            brandingId="org.argeo.cms.ui.workbench.rap.defaultBranding">
+      </entrypoint>
+      <entrypoint
+            brandingId="org.argeo.cms.ui.workbench.rap.defaultBranding"
+            class="org.argeo.cms.ui.workbench.rap.SpnegoWorkbenchLogin"
+            id="org.argeo.cms.ui.workbench.rap.loginEntryPoint"
+            path="/login">
+      </entrypoint>
+<!--      <entrypoint
+            id="org.argeo.cms.ui.workbench.rap.secureEntryPoint"
+            class="org.argeo.security.ui.rap.RapWorkbenchLogin"
+            path="/login"
+            brandingId="org.argeo.cms.ui.workbench.rap.defaultBranding">
+      </entrypoint> -->
+   </extension>
+
+       <!-- COMMANDS --> 
+       <extension point="org.eclipse.ui.commands">
+               <command
+                       id="org.argeo.cms.ui.workbench.rap.mainMenuCommand"
+                       defaultHandler="org.argeo.cms.ui.workbench.rap.commands.OpenHome"
+                       name="Main"> 
+               </command>
+               <command
+                       id="org.argeo.cms.ui.workbench.rap.openChangePasswordDialog"
+                       defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+                       name="%changePassword">
+               </command>
+               <!-- Enable an "open file" action in a single sourced application  -->  
+               <command
+                       id="org.argeo.cms.ui.workbench.openFile"
+                       defaultHandler="org.argeo.eclipse.spring.SpringExtensionFactory"
+                       name="OpenFile">
+                       <commandParameter
+                       id="param.fileName"
+                       name="The name of the file to open (optional)">
+                       </commandParameter>
+            <commandParameter
+                       id="param.fileURI"
+                       name="The URI of this file on the server">
+                       </commandParameter>
+               </command>
+       </extension>
+
+       <!-- MENUS --> 
+       <extension point="org.eclipse.ui.menus">
+       <!-- Main tool bar menu -->
+       <!--
+       <menuContribution locationURI="toolbar:org.eclipse.ui.main.toolbar">
+               <toolbar id="org.argeo.cms.ui.workbench.rap.userToolbar">
+                               <command
+                                       commandId="org.argeo.cms.ui.workbench.rap.mainMenuCommand"
+                                       icon="icons/home.gif"
+                                       id="org.argeo.cms.ui.workbench.rap.mainMenu"
+                                       style="pulldown">
+                               </command>
+                               <command commandId="org.eclipse.ui.file.save"/>
+                               <command commandId="org.eclipse.ui.file.saveAll"/>
+                       </toolbar>
+               </menuContribution>
+               -->
+       <menuContribution locationURI="toolbar:org.eclipse.ui.main.toolbar">
+               <toolbar id="org.argeo.cms.ui.workbench.userToolbar">
+                               <command
+                                       commandId="org.argeo.cms.ui.workbench.rap.mainMenuCommand"
+                                       icon="icons/home.png"
+                                       id="org.argeo.cms.ui.workbench.rap.mainMenu"
+                                       style="pulldown">
+                               </command>
+                               <command commandId="org.eclipse.ui.file.save" icon="icons/save.png"/>
+                               <command commandId="org.eclipse.ui.file.saveAll" icon="icons/save-all.png"/>
+                       </toolbar>
+               </menuContribution>
+               
+               <!-- User drop down default menu -->
+               <menuContribution locationURI="menu:org.argeo.cms.ui.workbench.rap.mainMenu">
+                       <!-- Managed programmatically in the RapActionBarAdvisor to enable 
+                            the display of the current logged-in user id -->
+                       <command
+                               commandId="org.argeo.cms.ui.workbench.rap.userMenuCommand"
+                               icon="icons/person-logged-in.png"
+                               id="org.argeo.cms.ui.workbench.rap.userMenu">
+                       </command>
+                       <!-- Still unused
+                       <command
+                               commandId="org.eclipse.ui.window.preferences"
+                               icon="icons/preferences.png"/> -->
+               <command
+                               commandId="org.argeo.cms.ui.workbench.rap.openChangePasswordDialog"
+                               icon="icons/security.gif"
+                               label="%changePassword"/>
+                       <separator
+                               name="org.argeo.cms.ui.workbench.rap.beforeFile"
+                               visible="true">
+                       </separator>
+                       <command commandId="org.eclipse.ui.file.closeAll" icon="icons/close-all.png"/>
+                       <command commandId="org.eclipse.ui.file.save" icon="icons/save.png"/>
+                       <command commandId="org.eclipse.ui.file.saveAll" icon="icons/save-all.png"/>
+       
+                       <!--<command commandId="org.eclipse.ui.views.showView"/>-->
+               <command commandId="org.eclipse.ui.perspectives.showPerspective"/>
+                       <separator
+                               name="org.argeo.cms.ui.workbench.rap.beforeExit"
+                               visible="true">
+                       </separator>
+                       <command commandId="org.eclipse.ui.file.exit" icon="icons/exit.png"/>
+               </menuContribution>
+       </extension>
+               
+    <!-- SERVICE HANDLERS --> 
+       <extension point="org.eclipse.rap.ui.serviceHandler">
+               <!-- Rap specific service handler to enable file download over the internet-->
+               <serviceHandler
+                       class="org.argeo.eclipse.ui.specific.OpenFileService"
+                       id="org.argeo.security.ui.specific.openFileService">
+               </serviceHandler>
+       </extension>
+    
+    <!-- ACTIVITIES -->
+       <extension
+           point="org.eclipse.ui.activities">
+        <activity
+              description="Anonymous"
+              id="org.argeo.cms.ui.workbench.rap.anonymousActivity"
+              name="Anonymous">
+                 <enabledWhen>
+                   <with variable="roles">
+                     <iterate ifEmpty="false" operator="or">
+                       <equals value="cn=anonymous,ou=roles,ou=node" />
+                     </iterate>
+                   </with>
+                 </enabledWhen>
+        </activity>
+        <activity
+              description="Not anonymous"
+              id="org.argeo.cms.ui.workbench.rap.notAnonymousActivity"
+              name="NotAnonymous">
+                 <enabledWhen>
+                       <not>
+                   <with variable="roles">
+                     <iterate ifEmpty="false" operator="or">
+                       <equals value="cn=anonymous,ou=roles,ou=node" />
+                     </iterate>
+                   </with>
+                   </not>
+                 </enabledWhen>
+        </activity>
+               <activityPatternBinding
+              activityId="org.argeo.cms.ui.workbench.rap.notAnonymousActivity"
+              pattern="org.argeo.cms.ui.workbench.rap/org.argeo.cms.ui.workbench.rap.userMenuCommand"/>         
+        <activityPatternBinding
+              activityId="org.argeo.cms.ui.workbench.rap.notAnonymousActivity"
+              pattern="org.argeo.cms.ui.workbench.rap/org.eclipse.ui.window.preferences"/>
+        <activityPatternBinding
+              activityId="org.argeo.cms.ui.workbench.rap.notAnonymousActivity"
+              pattern="org.argeo.cms.ui.workbench.rap/org.argeo.cms.ui.workbench.rap.openChangePasswordDialog"/>
+     </extension>
+    
+    <!-- BRANDINGS --> 
+     <extension
+         point="org.eclipse.rap.ui.branding">
+       <branding
+                       id="org.argeo.cms.ui.workbench.rap.defaultBranding"
+            themeId="org.eclipse.rap.rwt.theme.Default"
+            title="Argeo Web UI"
+            favicon="branding/favicon.ico">
+       </branding>
+       <!-- we need a servlet with this name j_spring_security_logout
+                for the logout filter -->
+       <branding
+                       id="org.argeo.cms.ui.workbench.rap.logoutBranding"
+            title="Argeo Logout"
+            favicon="branding/favicon.ico"
+            body="branding/empty.html">
+       </branding>
+       </extension>
+</plugin>
diff --git a/org.argeo.cms.ui.workbench.rap/pom.xml b/org.argeo.cms.ui.workbench.rap/pom.xml
new file mode 100644 (file)
index 0000000..6ace304
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>argeo-commons</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.cms.ui.workbench.rap</artifactId>
+       <name>CMS Workbench RAP</name>
+       <packaging>jar</packaging>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui.workbench</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <!-- RAP specific -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <dependency>
+                       <groupId>org.argeo.tp</groupId>
+                       <artifactId>argeo-tp-rap-e3</artifactId>
+                       <version>${version.argeo-tp}</version>
+                       <type>pom</type>
+                       <scope>provided</scope>
+               </dependency>
+
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/AnonymousEntryPoint.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/AnonymousEntryPoint.java
new file mode 100644 (file)
index 0000000..f657ec3
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.rap;
+
+import java.security.PrivilegedAction;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.node.NodeConstants;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.application.EntryPoint;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * RAP entry point which authenticates the subject as anonymous, for public
+ * unauthenticated access.
+ */
+public class AnonymousEntryPoint implements EntryPoint {
+       private final static Log log = LogFactory.getLog(AnonymousEntryPoint.class);
+
+       /**
+        * How many seconds to wait before invalidating the session if the user has
+        * not yet logged in.
+        */
+       private Integer sessionTimeout = 5 * 60;
+
+       @Override
+       public int createUI() {
+               RWT.getRequest().getSession().setMaxInactiveInterval(sessionTimeout);
+
+               // if (log.isDebugEnabled())
+               // log.debug("Anonymous THREAD=" + Thread.currentThread().getId()
+               // + ", sessionStore=" + RWT.getSessionStore().getId());
+
+               final Display display = PlatformUI.createDisplay();
+               Subject subject = new Subject();
+
+               final LoginContext loginContext;
+               try {
+                       loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS,
+                                       subject);
+                       loginContext.login();
+               } catch (LoginException e1) {
+                       throw new CmsException("Cannot initialize login context", e1);
+               }
+
+               // identify after successful login
+               if (log.isDebugEnabled())
+                       log.debug("Authenticated " + subject);
+               final String username = subject.getPrincipals().iterator().next()
+                               .getName();
+
+               // Logout callback when the display is disposed
+               display.disposeExec(new Runnable() {
+                       public void run() {
+                               log.debug("Display disposed");
+                               logout(loginContext, username);
+                       }
+               });
+
+               //
+               // RUN THE WORKBENCH
+               //
+               Integer returnCode = null;
+               try {
+                       returnCode = Subject.doAs(subject, new PrivilegedAction<Integer>() {
+                               public Integer run() {
+                                       RapWorkbenchAdvisor workbenchAdvisor = new RapWorkbenchAdvisor(
+                                                       null);
+                                       int result = PlatformUI.createAndRunWorkbench(display,
+                                                       workbenchAdvisor);
+                                       return new Integer(result);
+                               }
+                       });
+                       logout(loginContext, username);
+                       if (log.isTraceEnabled())
+                               log.trace("Return code " + returnCode);
+               } finally {
+                       display.dispose();
+               }
+               return 1;
+       }
+
+       private void logout(LoginContext loginContext, String username) {
+               try {
+                       loginContext.logout();
+                       log.info("Logged out " + (username != null ? username : "")
+                                       + " (THREAD=" + Thread.currentThread().getId() + ")");
+               } catch (LoginException e) {
+                       log.error("Erorr when logging out", e);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapActionBarAdvisor.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapActionBarAdvisor.java
new file mode 100644 (file)
index 0000000..c18a9a7
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.rap;
+
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.workbench.commands.OpenHomePerspective;
+import org.eclipse.core.commands.Category;
+import org.eclipse.core.commands.Command;
+import org.eclipse.jface.action.ICoolBarManager;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
+import org.eclipse.ui.application.ActionBarAdvisor;
+import org.eclipse.ui.application.IActionBarConfigurer;
+import org.eclipse.ui.commands.ICommandService;
+
+/** Eclipse rap specific action bar advisor */
+public class RapActionBarAdvisor extends ActionBarAdvisor {
+       private final static String ID_BASE = SecureRapActivator.ID;
+       // private final static Log log = LogFactory
+       // .getLog(SecureActionBarAdvisor.class);
+
+       /** Null means anonymous */
+       private String username = null;
+
+       // private IAction logoutAction;
+       // private IWorkbenchAction openPerspectiveDialogAction;
+       // private IWorkbenchAction showViewMenuAction;
+       // private IWorkbenchAction preferences;
+       private IWorkbenchAction saveAction;
+       private IWorkbenchAction saveAllAction;
+
+       // private IWorkbenchAction closeAllAction;
+
+       public RapActionBarAdvisor(IActionBarConfigurer configurer, String username) {
+               super(configurer);
+               this.username = username;
+       }
+
+       protected void makeActions(IWorkbenchWindow window) {
+               // preferences = ActionFactory.PREFERENCES.create(window);
+               // register(preferences);
+               // openPerspectiveDialogAction = ActionFactory.OPEN_PERSPECTIVE_DIALOG
+               // .create(window);
+               // register(openPerspectiveDialogAction);
+               // showViewMenuAction = ActionFactory.SHOW_VIEW_MENU.create(window);
+               // register(showViewMenuAction);
+               //
+               // // logout
+               // logoutAction = ActionFactory.QUIT.create(window);
+               // // logoutAction = createLogoutAction();
+               // register(logoutAction);
+               //
+               // Save semantics
+               saveAction = ActionFactory.SAVE.create(window);
+               register(saveAction);
+               saveAllAction = ActionFactory.SAVE_ALL.create(window);
+               register(saveAllAction);
+               // closeAllAction = ActionFactory.CLOSE_ALL.create(window);
+               // register(closeAllAction);
+
+       }
+
+       protected void fillMenuBar(IMenuManager menuBar) {
+               // MenuManager fileMenu = new MenuManager("&File",
+               // IWorkbenchActionConstants.M_FILE);
+               // MenuManager editMenu = new MenuManager("&Edit",
+               // IWorkbenchActionConstants.M_EDIT);
+               // MenuManager windowMenu = new MenuManager("&Window",
+               // IWorkbenchActionConstants.M_WINDOW);
+               //
+               // menuBar.add(fileMenu);
+               // menuBar.add(editMenu);
+               // menuBar.add(windowMenu);
+               // // Add a group marker indicating where action set menus will appear.
+               // menuBar.add(new GroupMarker(IWorkbenchActionConstants.MB_ADDITIONS));
+               //
+               // // File
+               // fileMenu.add(saveAction);
+               // fileMenu.add(saveAllAction);
+               // fileMenu.add(closeAllAction);
+               // fileMenu.add(new
+               // GroupMarker(IWorkbenchActionConstants.MB_ADDITIONS));
+               // fileMenu.add(new Separator());
+               // fileMenu.add(logoutAction);
+               //
+               // // Edit
+               // editMenu.add(preferences);
+               //
+               // // Window
+               // windowMenu.add(openPerspectiveDialogAction);
+               // windowMenu.add(showViewMenuAction);
+       }
+
+       @Override
+       protected void fillCoolBar(ICoolBarManager coolBar) {
+               // Add a command which label is the display name of the current
+               // logged-in user
+               if (username != null) {
+                       ICommandService cmdService = (ICommandService) getActionBarConfigurer()
+                                       .getWindowConfigurer().getWorkbenchConfigurer()
+                                       .getWorkbench().getService(ICommandService.class);
+                       Category userMenus = cmdService.getCategory(ID_BASE + ".userMenus");
+                       if (!userMenus.isDefined())
+                               userMenus.define("User Menus", "User related menus");
+                       Command userMenu = cmdService.getCommand(ID_BASE
+                                       + ".userMenuCommand");
+                       if (userMenu.isDefined())
+                               userMenu.undefine();
+                       userMenu.define(CurrentUser.getDisplayName(), "User menu actions",
+                                       userMenus);
+                       // userMenu.define(username, "User menu actions", userMenus);
+                       
+                       userMenu.setHandler(new OpenHomePerspective());
+
+                       // userToolbar.add(new UserMenuAction());
+                       // coolBar.add(userToolbar);
+               } else {// anonymous
+                       IToolBarManager userToolbar = new ToolBarManager(SWT.FLAT
+                                       | SWT.RIGHT);
+                       // userToolbar.add(logoutAction);
+                       coolBar.add(userToolbar);
+               }
+               // IToolBarManager saveToolbar = new ToolBarManager(SWT.FLAT |
+               // SWT.RIGHT);
+               // saveToolbar.add(saveAction);
+               // saveToolbar.add(saveAllAction);
+               // coolBar.add(saveToolbar);
+       }
+
+       // class UserMenuAction extends Action implements IWorkbenchAction {
+       //
+       // public UserMenuAction() {
+       // super(username, IAction.AS_DROP_DOWN_MENU);
+       // // setMenuCreator(new UserMenu());
+       // }
+       //
+       // @Override
+       // public String getId() {
+       // return "org.argeo.cms.ui.workbench.rap.userMenu";
+       // }
+       //
+       // @Override
+       // public void dispose() {
+       // }
+       //
+       // }
+
+       // class UserMenu implements IMenuCreator {
+       // private Menu menu;
+       //
+       // public Menu getMenu(Control parent) {
+       // Menu menu = new Menu(parent);
+       // addActionToMenu(menu, logoutAction);
+       // return menu;
+       // }
+       //
+       // private void addActionToMenu(Menu menu, IAction action) {
+       // ActionContributionItem item = new ActionContributionItem(action);
+       // item.fill(menu, -1);
+       // }
+       //
+       // public void dispose() {
+       // if (menu != null) {
+       // menu.dispose();
+       // }
+       // }
+       //
+       // public Menu getMenu(Menu parent) {
+       // // Not use
+       // return null;
+       // }
+       //
+       // }
+
+       // protected IAction createLogoutAction() {
+       // Subject subject = Subject.getSubject(AccessController.getContext());
+       // final String username = subject.getPrincipals().iterator().next()
+       // .getName();
+       //
+       // IAction logoutAction = new Action() {
+       // public String getId() {
+       // return SecureRapActivator.ID + ".logoutAction";
+       // }
+       //
+       // public String getText() {
+       // return "Logout " + username;
+       // }
+       //
+       // public void run() {
+       // // try {
+       // // Subject subject = SecureRapActivator.getLoginContext()
+       // // .getSubject();
+       // // String subjectStr = subject.toString();
+       // // subject.getPrincipals().clear();
+       // // SecureRapActivator.getLoginContext().logout();
+       // // log.info(subjectStr + " logged out");
+       // // } catch (LoginException e) {
+       // // log.error("Error when logging out", e);
+       // // }
+       // // SecureEntryPoint.logout(username);
+       // // PlatformUI.getWorkbench().close();
+       // // try {
+       // // RWT.getRequest().getSession().setMaxInactiveInterval(1);
+       // // } catch (Exception e) {
+       // // if (log.isTraceEnabled())
+       // // log.trace("Error when invalidating session", e);
+       // // }
+       // }
+       //
+       // };
+       // return logoutAction;
+       // }
+
+}
diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWindowAdvisor.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWindowAdvisor.java
new file mode 100644 (file)
index 0000000..60bad09
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.rap;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
+import org.eclipse.ui.application.ActionBarAdvisor;
+import org.eclipse.ui.application.IActionBarConfigurer;
+import org.eclipse.ui.application.IWorkbenchWindowConfigurer;
+import org.eclipse.ui.application.WorkbenchWindowAdvisor;
+
+/** Eclipse RAP specific window advisor */
+public class RapWindowAdvisor extends WorkbenchWindowAdvisor {
+
+       private String username;
+
+       public RapWindowAdvisor(IWorkbenchWindowConfigurer configurer,
+                       String username) {
+               super(configurer);
+               this.username = username;
+       }
+
+       @Override
+       public ActionBarAdvisor createActionBarAdvisor(
+                       IActionBarConfigurer configurer) {
+               return new RapActionBarAdvisor(configurer, username);
+       }
+
+       public void preWindowOpen() {
+               IWorkbenchWindowConfigurer configurer = getWindowConfigurer();
+               configurer.setShowCoolBar(true);
+               configurer.setShowMenuBar(false);
+               configurer.setShowStatusLine(false);
+               configurer.setShowPerspectiveBar(true);
+               configurer.setTitle("Argeo Web UI"); //$NON-NLS-1$
+               // Full screen, see
+               // http://wiki.eclipse.org/RAP/FAQ#How_to_create_a_fullscreen_application
+               configurer.setShellStyle(SWT.NO_TRIM);
+               Rectangle bounds = Display.getCurrent().getBounds();
+               configurer.setInitialSize(new Point(bounds.width, bounds.height));
+
+               // Handle window resize in Rap 2.1+ see
+               // https://bugs.eclipse.org/bugs/show_bug.cgi?id=417254
+               Display.getCurrent().addListener(SWT.Resize, new Listener() {
+                       private static final long serialVersionUID = 2970912561866704526L;
+
+                       @Override
+                       public void handleEvent(Event event) {
+                               Rectangle bounds = event.display.getBounds();
+                               IWorkbenchWindow iww = getWindowConfigurer().getWindow()
+                                               .getWorkbench().getActiveWorkbenchWindow();
+                               iww.getShell().setBounds(bounds);
+                       }
+               });
+       }
+
+       @Override
+       public void postWindowCreate() {
+               Shell shell = getWindowConfigurer().getWindow().getShell();
+               shell.setMaximized(true);
+       }
+
+       @Override
+       public void postWindowOpen() {
+               String defaultPerspective = getWindowConfigurer()
+                               .getWorkbenchConfigurer().getWorkbench()
+                               .getPerspectiveRegistry().getDefaultPerspective();
+               if (defaultPerspective == null) {
+                       IWorkbenchWindow window = getWindowConfigurer().getWindow();
+                       if (window == null)
+                               return;
+
+                       IWorkbenchAction openPerspectiveDialogAction = ActionFactory.OPEN_PERSPECTIVE_DIALOG
+                                       .create(window);
+                       openPerspectiveDialogAction.run();
+               }
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWorkbenchAdvisor.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWorkbenchAdvisor.java
new file mode 100644 (file)
index 0000000..b650dbb
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.rap;
+
+import org.eclipse.ui.IPerspectiveDescriptor;
+import org.eclipse.ui.application.IWorkbenchConfigurer;
+import org.eclipse.ui.application.IWorkbenchWindowConfigurer;
+import org.eclipse.ui.application.WorkbenchAdvisor;
+import org.eclipse.ui.application.WorkbenchWindowAdvisor;
+
+/** Eclipse RAP specific workbench advisor */
+public class RapWorkbenchAdvisor extends WorkbenchAdvisor {
+       public final static String INITIAL_PERSPECTIVE_PROPERTY = "org.argeo.security.ui.initialPerspective";
+       public final static String SAVE_AND_RESTORE_PROPERTY = "org.argeo.security.ui.saveAndRestore";
+
+       private String initialPerspective = System.getProperty(
+                       INITIAL_PERSPECTIVE_PROPERTY, null);
+
+       private String username;
+
+       public RapWorkbenchAdvisor(String username) {
+               this.username = username;
+       }
+
+       @Override
+       public void initialize(IWorkbenchConfigurer configurer) {
+               super.initialize(configurer);
+               Boolean saveAndRestore = Boolean.parseBoolean(System.getProperty(
+                               SAVE_AND_RESTORE_PROPERTY, "false"));
+               configurer.setSaveAndRestore(saveAndRestore);
+       }
+
+       public WorkbenchWindowAdvisor createWorkbenchWindowAdvisor(
+                       IWorkbenchWindowConfigurer configurer) {
+               return new RapWindowAdvisor(configurer, username);
+       }
+
+       public String getInitialWindowPerspectiveId() {
+               if (initialPerspective != null) {
+                       // check whether this user can see the declared perspective
+                       // (typically the perspective won't be listed if this user doesn't
+                       // have the right to see it)
+                       IPerspectiveDescriptor pd = getWorkbenchConfigurer().getWorkbench()
+                                       .getPerspectiveRegistry()
+                                       .findPerspectiveWithId(initialPerspective);
+                       if (pd == null)
+                               return null;
+               }
+               return initialPerspective;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWorkbenchLogin.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/RapWorkbenchLogin.java
new file mode 100644 (file)
index 0000000..73aac82
--- /dev/null
@@ -0,0 +1,90 @@
+package org.argeo.cms.ui.workbench.rap;
+
+import java.security.PrivilegedAction;
+import java.util.Locale;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+
+import org.argeo.cms.CmsMsg;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.util.LoginEntryPoint;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.PlatformUI;
+
+public class RapWorkbenchLogin extends LoginEntryPoint {
+       // private final static Log log =
+       // LogFactory.getLog(RapWorkbenchLogin.class);
+
+       /** Override to provide an application specific workbench advisor */
+       protected RapWorkbenchAdvisor createRapWorkbenchAdvisor(String username) {
+               return new RapWorkbenchAdvisor(username);
+       }
+
+       @Override
+       public int createUI() {
+               JavaScriptExecutor jsExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
+               int returnCode;
+               try {
+                       returnCode = super.createUI();
+               } finally {
+                       // always reload
+                       // TODO optimise?
+                       jsExecutor.execute("location.reload()");
+               }
+               return returnCode;
+       }
+
+       @Override
+       protected int postLogin() {
+               Subject subject = getSubject();
+               final Display display = Display.getCurrent();
+               if (subject.getPrincipals(X500Principal.class).isEmpty()) {
+                       RWT.getClient().getService(JavaScriptExecutor.class).execute("location.reload()");
+               }
+               //
+               // RUN THE WORKBENCH
+               //
+               Integer returnCode = null;
+               try {
+                       returnCode = Subject.doAs(subject, new PrivilegedAction<Integer>() {
+                               public Integer run() {
+                                       int result = createAndRunWorkbench(display, CurrentUser.getUsername(subject));
+                                       return new Integer(result);
+                               }
+                       });
+                       // explicit workbench closing
+                       logout();
+               } finally {
+                       display.dispose();
+               }
+               return returnCode;
+       }
+
+       protected int createAndRunWorkbench(Display display, String username) {
+               RapWorkbenchAdvisor workbenchAdvisor = createRapWorkbenchAdvisor(username);
+               return PlatformUI.createAndRunWorkbench(display, workbenchAdvisor);
+       }
+
+       @Override
+       protected void extendsCredentialsBlock(Composite credentialsBlock, Locale selectedLocale,
+                       SelectionListener loginSelectionListener) {
+               Button loginButton = new Button(credentialsBlock, SWT.PUSH);
+               loginButton.setText(CmsMsg.login.lead(selectedLocale));
+               loginButton.setLayoutData(CmsUtils.fillWidth());
+               loginButton.addSelectionListener(loginSelectionListener);
+       }
+
+       @Override
+       protected Display createDisplay() {
+               return PlatformUI.createDisplay();
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/SecureRapActivator.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/SecureRapActivator.java
new file mode 100644 (file)
index 0000000..74068c2
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.rap;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/** Configure Equinox login context from the bundle context. */
+public class SecureRapActivator implements BundleActivator {
+       public final static String ID = "org.argeo.cms.ui.workbench.rap";
+
+       private static BundleContext bundleContext;
+
+       public void start(BundleContext bc) throws Exception {
+               bundleContext = bc;
+       }
+
+       public void stop(BundleContext context) throws Exception {
+               bundleContext = null;
+       }
+
+       public static BundleContext getBundleContext() {
+               return bundleContext;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/SpnegoWorkbenchLogin.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/SpnegoWorkbenchLogin.java
new file mode 100644 (file)
index 0000000..2f4ef09
--- /dev/null
@@ -0,0 +1,99 @@
+package org.argeo.cms.ui.workbench.rap;
+
+import java.security.PrivilegedAction;
+import java.util.Locale;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.cms.CmsMsg;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.util.LoginEntryPoint;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.PlatformUI;
+
+public class SpnegoWorkbenchLogin extends LoginEntryPoint {
+       // private final static Log log =
+       // LogFactory.getLog(RapWorkbenchLogin.class);
+
+       /** Override to provide an application specific workbench advisor */
+       protected RapWorkbenchAdvisor createRapWorkbenchAdvisor(String username) {
+               return new RapWorkbenchAdvisor(username);
+       }
+
+       @Override
+       public int createUI() {
+               HttpServletRequest request = RWT.getRequest();
+               String authorization = request.getHeader(HEADER_AUTHORIZATION);
+               if (authorization == null || !authorization.startsWith("Negotiate")) {
+                       HttpServletResponse response = RWT.getResponse();
+                       response.setStatus(401);
+                       response.setHeader(HEADER_WWW_AUTHENTICATE, "Negotiate");
+                       response.setDateHeader("Date", System.currentTimeMillis());
+                       response.setDateHeader("Expires", System.currentTimeMillis() + (24 * 60 * 60 * 1000));
+                       response.setHeader("Accept-Ranges", "bytes");
+                       response.setHeader("Connection", "Keep-Alive");
+                       response.setHeader("Keep-Alive", "timeout=5, max=97");
+                       // response.setContentType("text/html; charset=UTF-8");
+               }
+
+               int returnCode;
+               returnCode = super.createUI();
+               return returnCode;
+       }
+
+       @Override
+       protected int postLogin() {
+               Subject subject = getSubject();
+               final Display display = Display.getCurrent();
+               if (subject.getPrincipals(X500Principal.class).isEmpty()) {
+                       RWT.getClient().getService(JavaScriptExecutor.class).execute("location.reload()");
+               }
+               //
+               // RUN THE WORKBENCH
+               //
+               Integer returnCode = null;
+               try {
+                       returnCode = Subject.doAs(subject, new PrivilegedAction<Integer>() {
+                               public Integer run() {
+                                       int result = createAndRunWorkbench(display, CurrentUser.getUsername(subject));
+                                       return new Integer(result);
+                               }
+                       });
+                       // explicit workbench closing
+                       logout();
+               } finally {
+                       display.dispose();
+               }
+               return returnCode;
+       }
+
+       protected int createAndRunWorkbench(Display display, String username) {
+               RapWorkbenchAdvisor workbenchAdvisor = createRapWorkbenchAdvisor(username);
+               return PlatformUI.createAndRunWorkbench(display, workbenchAdvisor);
+       }
+
+       @Override
+       protected void extendsCredentialsBlock(Composite credentialsBlock, Locale selectedLocale,
+                       SelectionListener loginSelectionListener) {
+               Button loginButton = new Button(credentialsBlock, SWT.PUSH);
+               loginButton.setText(CmsMsg.login.lead(selectedLocale));
+               loginButton.setLayoutData(CmsUtils.fillWidth());
+               loginButton.addSelectionListener(loginSelectionListener);
+       }
+
+       @Override
+       protected Display createDisplay() {
+               return PlatformUI.createDisplay();
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/commands/OpenHome.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/commands/OpenHome.java
new file mode 100644 (file)
index 0000000..86e0103
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.rap.commands;
+
+import org.argeo.cms.ui.workbench.UserHomePerspective;
+import org.argeo.cms.ui.workbench.util.CommandUtils;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.ui.WorkbenchException;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Default action of the user menu */
+public class OpenHome extends AbstractHandler {
+       private final static String PROP_OPEN_HOME_CMD_ID = "org.argeo.ui.openHomeCommandId";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+
+               String defaultCmdId = System.getProperty(PROP_OPEN_HOME_CMD_ID, "");
+               if (!"".equals(defaultCmdId.trim()))
+                       CommandUtils.callCommand(defaultCmdId);
+               else {
+                       try {
+                               String defaultPerspective = HandlerUtil.getActiveWorkbenchWindow(event).getWorkbench()
+                                               .getPerspectiveRegistry().getDefaultPerspective();
+                               HandlerUtil.getActiveSite(event).getWorkbenchWindow()
+                                               .openPage(defaultPerspective != null ? defaultPerspective : UserHomePerspective.ID, null);
+                       } catch (WorkbenchException e) {
+                               ErrorFeedback.show("Cannot open home perspective", e);
+                       }
+               }
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/commands/UserMenu.java b/org.argeo.cms.ui.workbench.rap/src/org/argeo/cms/ui/workbench/rap/commands/UserMenu.java
new file mode 100644 (file)
index 0000000..4934e56
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.rap.commands;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+
+/** Default action of the user menu */
+public class UserMenu extends AbstractHandler {
+
+       @Override
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               return null;
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench/.classpath b/org.argeo.cms.ui.workbench/.classpath
new file mode 100644 (file)
index 0000000..457b115
--- /dev/null
@@ -0,0 +1,9 @@
+<?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-1.8" />
+       <classpathentry kind="output" path="bin" />
+</classpath>
diff --git a/org.argeo.cms.ui.workbench/.gitignore b/org.argeo.cms.ui.workbench/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.cms.ui.workbench/.project b/org.argeo.cms.ui.workbench/.project
new file mode 100644 (file)
index 0000000..f7f7a8e
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.ui.workbench</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.cms.ui.workbench/META-INF/.gitignore b/org.argeo.cms.ui.workbench/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.cms.ui.workbench/META-INF/spring/commands.xml b/org.argeo.cms.ui.workbench/META-INF/spring/commands.xml
new file mode 100644 (file)
index 0000000..1c74f7a
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd">
+
+       <!-- USERS CRUDS -->
+       <bean id="newUser" class="org.argeo.cms.ui.workbench.internal.useradmin.commands.NewUser"
+               scope="prototype">
+               <property name="userAdminWrapper" ref="userAdminWrapper" />
+       </bean>
+       <bean id="deleteUsers"
+               class="org.argeo.cms.ui.workbench.internal.useradmin.commands.DeleteUsers"
+               scope="prototype">
+               <property name="userAdminWrapper" ref="userAdminWrapper" />
+       </bean>
+       <bean id="userBatchUpdate"
+               class="org.argeo.cms.ui.workbench.internal.useradmin.commands.UserBatchUpdate"
+               scope="prototype">
+               <property name="userAdminWrapper" ref="userAdminWrapper" />
+       </bean>
+       <!-- GROUPS CRUDS -->
+       <bean id="newGroup" class="org.argeo.cms.ui.workbench.internal.useradmin.commands.NewGroup"
+               scope="prototype">
+               <property name="userAdminWrapper" ref="userAdminWrapper" />
+       </bean>
+       <bean id="deleteGroups"
+               class="org.argeo.cms.ui.workbench.internal.useradmin.commands.DeleteGroups"
+               scope="prototype">
+               <property name="userAdminWrapper" ref="userAdminWrapper" />
+       </bean>
+
+       <!-- TRANSACTIONS -->
+       <bean id="userTransactionHandler"
+               class="org.argeo.cms.ui.workbench.internal.useradmin.commands.UserTransactionHandler"
+               scope="prototype">
+               <property name="userAdminWrapper" ref="userAdminWrapper" />
+       </bean>
+       
+       <!-- DATA EXPLORER -->
+       <bean id="addRemoteRepository"
+               class="org.argeo.cms.ui.workbench.internal.jcr.commands.AddRemoteRepository">
+               <property name="repositoryFactory" ref="repositoryFactory" />
+               <property name="nodeRepository" ref="nodeRepository" />
+               <property name="keyring" ref="keyring" />
+       </bean>
+
+       <bean id="addPrivileges" class="org.argeo.cms.ui.workbench.internal.jcr.commands.AddPrivileges">
+               <property name="userAdmin" ref="userAdmin" />
+       </bean>
+       <bean id="removePrivileges"
+               class="org.argeo.cms.ui.workbench.internal.jcr.commands.RemovePrivileges">
+               <!-- <property name="userAdmin" ref="userAdmin" /> -->
+       </bean>
+</beans>
diff --git a/org.argeo.cms.ui.workbench/META-INF/spring/common.xml b/org.argeo.cms.ui.workbench/META-INF/spring/common.xml
new file mode 100644 (file)
index 0000000..32a3a8f
--- /dev/null
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<beans xmlns="http://www.springframework.org/schema/beans"\r
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\r
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">\r
+\r
+       <bean id="userTransactionProvider"\r
+               class="org.argeo.cms.ui.workbench.internal.useradmin.providers.UserTransactionProvider"\r
+               scope="singleton" lazy-init="false">\r
+               <property name="userTransaction" ref="userTransaction" />\r
+       </bean>\r
+\r
+       <bean id="userAdminWrapper"\r
+               class="org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper"\r
+               scope="singleton" lazy-init="false">\r
+               <property name="userTransaction" ref="userTransaction" />\r
+               <property name="userAdmin" ref="userAdmin" />\r
+               <property name="userAdminServiceReference" ref="userAdmin" />\r
+       </bean>\r
+\r
+       <bean id="repositoryRegister" class="org.argeo.cms.ui.jcr.DefaultRepositoryRegister" />\r
+\r
+</beans>\r
diff --git a/org.argeo.cms.ui.workbench/META-INF/spring/osgi.xml b/org.argeo.cms.ui.workbench/META-INF/spring/osgi.xml
new file mode 100644 (file)
index 0000000..a322412
--- /dev/null
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<beans:beans xmlns="http://www.springframework.org/schema/osgi"\r
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"\r
+       xmlns:osgi="http://www.springframework.org/schema/osgi"\r
+       xsi:schemaLocation="http://www.springframework.org/schema/osgi  \r
+       http://www.springframework.org/schema/osgi/spring-osgi-1.1.xsd\r
+       http://www.springframework.org/schema/beans   \r
+       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"\r
+       osgi:default-timeout="30000">\r
+\r
+       <!-- JCR -->\r
+       <reference id="repositoryFactory" interface="javax.jcr.RepositoryFactory" />\r
+       <reference id="keyring" interface="org.argeo.node.security.CryptoKeyring" />\r
+       <list id="repositories" interface="javax.jcr.Repository"\r
+               cardinality="0..N">\r
+               <listener ref="repositoryRegister" bind-method="register"\r
+                       unbind-method="unregister" />\r
+       </list>\r
+\r
+       <reference id="nodeRepository" interface="javax.jcr.Repository"\r
+               filter="(cn=home)" />\r
+               \r
+       <reference id="nodeInstance" interface="org.argeo.node.NodeInstance" />\r
+\r
+       <reference id="nodeFileSystemProvider" interface="java.nio.file.spi.FileSystemProvider"\r
+               filter="(service.pid=org.argeo.node.fsProvider)" />\r
+\r
+       <!-- UserAdmin -->\r
+       <reference id="userAdmin" interface="org.osgi.service.useradmin.UserAdmin" />\r
+       <reference id="userTransaction" interface="javax.transaction.UserTransaction" />\r
+\r
+\r
+       <reference id="secureLogger" interface="org.argeo.node.ArgeoLogger"\r
+               cardinality="0..1" />\r
+       <reference id="defaultCallbackHandler" interface="javax.security.auth.callback.CallbackHandler" />\r
+\r
+</beans:beans>\r
diff --git a/org.argeo.cms.ui.workbench/META-INF/spring/parts.xml b/org.argeo.cms.ui.workbench/META-INF/spring/parts.xml
new file mode 100644 (file)
index 0000000..a884d51
--- /dev/null
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd">
+
+       <!-- SECURITY -->
+       <!-- Editors -->
+       <bean id="userEditor"
+               class="org.argeo.cms.ui.workbench.internal.useradmin.parts.UserEditor"
+               scope="prototype">
+               <property name="userAdminWrapper" ref="userAdminWrapper" />
+       </bean>
+
+       <bean id="groupEditor"
+               class="org.argeo.cms.ui.workbench.internal.useradmin.parts.UserEditor"
+               scope="prototype">
+               <property name="userAdminWrapper" ref="userAdminWrapper" />
+               <property name="repository" ref="nodeRepository" />
+               <property name="nodeInstance" ref="nodeInstance" />
+       </bean>
+       
+       <!-- Views -->
+       <bean id="usersView"
+               class="org.argeo.cms.ui.workbench.internal.useradmin.parts.UsersView"
+               scope="prototype">
+               <property name="userAdminWrapper" ref="userAdminWrapper" />
+       </bean>
+       <bean id="groupsView"
+               class="org.argeo.cms.ui.workbench.internal.useradmin.parts.GroupsView"
+               scope="prototype">
+               <property name="userAdminWrapper" ref="userAdminWrapper" />
+       </bean>
+
+
+       <!-- DATA EXPLORER -->
+       <!-- Editors -->
+       <bean id="genericJcrQueryEditor" class="org.argeo.cms.ui.workbench.jcr.GenericJcrQueryEditor"
+               scope="prototype">
+               <property name="nodeRepository" ref="nodeRepository" />
+       </bean>
+       <bean id="defaultNodeEditor" class="org.argeo.cms.ui.workbench.jcr.DefaultNodeEditor"
+               scope="prototype">
+       </bean>
+       <!-- Views -->
+       <bean id="jcrBrowserView" class="org.argeo.cms.ui.workbench.jcr.JcrBrowserView"
+               scope="prototype">
+               <property name="repositoryRegister" ref="repositoryRegister" />
+               <property name="repositoryFactory" ref="repositoryFactory" />
+               <property name="nodeRepository" ref="nodeRepository" />
+               <property name="keyring" ref="keyring" />
+       </bean>
+       <bean id="nodeFsBrowserView" class="org.argeo.cms.ui.workbench.jcr.NodeFsBrowserView"
+               scope="prototype">
+               <property name="nodeFileSystemProvider" ref="nodeFileSystemProvider" />
+               <!-- <property name="keyring" ref="keyring" /> -->
+       </bean>
+
+       <!-- LOGGERS -->
+       <bean id="logView" class="org.argeo.cms.ui.workbench.useradmin.LogView"
+               scope="prototype">
+               <property name="argeoLogger" ref="secureLogger" />
+       </bean>
+       <bean id="adminLogView" class="org.argeo.cms.ui.workbench.useradmin.AdminLogView"
+               scope="prototype">
+               <property name="argeoLogger" ref="secureLogger" />
+       </bean>
+</beans>
diff --git a/org.argeo.cms.ui.workbench/OSGI-INF/l10n/bundle.properties b/org.argeo.cms.ui.workbench/OSGI-INF/l10n/bundle.properties
new file mode 100644 (file)
index 0000000..3ec4305
--- /dev/null
@@ -0,0 +1 @@
+search=Finden
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench/OSGI-INF/l10n/bundle_de.properties b/org.argeo.cms.ui.workbench/OSGI-INF/l10n/bundle_de.properties
new file mode 100644 (file)
index 0000000..8c4ac22
--- /dev/null
@@ -0,0 +1 @@
+search=Search
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench/bnd.bnd b/org.argeo.cms.ui.workbench/bnd.bnd
new file mode 100644 (file)
index 0000000..83095b7
--- /dev/null
@@ -0,0 +1,26 @@
+Bundle-SymbolicName: org.argeo.cms.ui.workbench;singleton:=true
+Bundle-Activator: org.argeo.cms.ui.workbench.WorkbenchUiPlugin
+Bundle-ActivationPolicy: lazy
+
+Require-Bundle:        org.eclipse.core.runtime,\
+org.eclipse.core.commands
+
+Import-Package:        org.argeo.cms.auth,\
+org.argeo.cms.ui,\
+org.argeo.cms.i18n,\
+org.argeo.eclipse.spring,\
+org.argeo.eclipse.ui.utils,\
+org.eclipse.core.runtime.jobs,\
+org.eclipse.jface.window,\
+org.eclipse.swt,\
+org.eclipse.swt.widgets,\
+org.eclipse.ui.services,\
+org.osgi.*;version=0.0.0,\
+org.springframework.core,\
+org.springframework.beans.factory,\
+org.springframework.core.io.support,\
+!org.eclipse.core.runtime,\
+*                              
+
+
+# org.argeo.eclipse.ui.workbench;resolution:=optional,\
diff --git a/org.argeo.cms.ui.workbench/build.properties b/org.argeo.cms.ui.workbench/build.properties
new file mode 100644 (file)
index 0000000..1b9b7bd
--- /dev/null
@@ -0,0 +1,7 @@
+source.. =     src/
+output.. =  bin/
+bin.includes = META-INF/,\
+               .,\
+               icons/,\
+               plugin.xml
+additional.bundles = org.apache.commons.httpclient
diff --git a/org.argeo.cms.ui.workbench/keyring.properties b/org.argeo.cms.ui.workbench/keyring.properties
new file mode 100644 (file)
index 0000000..0228d47
--- /dev/null
@@ -0,0 +1 @@
+argeo.keyring.secreteKeyLength=256
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench/plugin.xml b/org.argeo.cms.ui.workbench/plugin.xml
new file mode 100644 (file)
index 0000000..f25331e
--- /dev/null
@@ -0,0 +1,807 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?eclipse version="3.4"?>
+<plugin>
+   <extension
+         point="org.eclipse.ui.perspectives">
+      <perspective
+            id="org.argeo.cms.ui.workbench.adminSecurityPerspective"
+            class="org.argeo.cms.ui.workbench.SecurityAdminPerspective"
+            icon="platform:/plugin/org.argeo.cms.ui.theme/platform:/plugin/org.argeo.cms.ui.theme/icons/group.png"
+            name="Security">
+      </perspective>
+      <perspective
+            id="org.argeo.cms.ui.workbench.userHomePerspective"
+            class="org.argeo.cms.ui.workbench.UserHomePerspective"
+            icon="platform:/plugin/org.argeo.cms.ui.theme/platform:/plugin/org.argeo.cms.ui.theme/icons/home.png"
+            name="Home">
+      </perspective>
+      <perspective
+            id="org.argeo.cms.ui.workbench.adminMaintenancePerspective"
+            class="org.argeo.cms.ui.workbench.MaintenancePerspective"
+            icon="platform:/plugin/org.argeo.cms.ui.theme/platform:/plugin/org.argeo.cms.ui.theme/icons/maintenance.gif"
+            name="Maintenance">
+      </perspective>
+      <perspective
+            id="org.argeo.cms.ui.workbench.osgiPerspective"
+            class="org.argeo.cms.ui.workbench.OsgiExplorerPerspective"
+            name="Monitoring"
+            icon="platform:/plugin/org.argeo.cms.ui.theme/platform:/plugin/org.argeo.cms.ui.theme/icons/osgi_explorer.gif">
+      </perspective>
+      <perspective
+            id="org.argeo.cms.ui.workbench.jcrBrowserPerspective"
+            class="org.argeo.cms.ui.workbench.JcrBrowserPerspective"
+            name="Data Explorer"
+            icon="platform:/plugin/org.argeo.cms.ui.theme/icons/nodes.gif">
+      </perspective>
+   </extension>
+   
+    <!-- Definition of the OSGI perspective -->
+    <extension point="org.eclipse.ui.perspectiveExtensions"> 
+        <perspectiveExtension targetID="org.argeo.cms.ui.workbench.osgiPerspective"> 
+            <view 
+               id="org.argeo.cms.ui.workbench.cmsSessionsView" 
+               minimized="false"
+               ratio="0.5" 
+               relationship="left" 
+               relative="org.eclipse.ui.editorss"/> 
+            <view 
+               id="org.argeo.cms.ui.workbench.modulesView" 
+               minimized="false"
+               relationship="stack"
+               relative="org.argeo.cms.ui.workbench.cmsSessionsView"/> 
+             <view 
+               id="org.argeo.cms.ui.workbench.bundlesView" 
+               minimized="false"
+               relationship="stack" 
+               relative="org.argeo.cms.ui.workbench.modulesView"/> 
+             <view 
+               id="org.argeo.cms.ui.workbench.multiplePackagesView" 
+               minimized="false"
+               relationship="stack" 
+               relative="org.argeo.cms.ui.workbench.bundlesView"/> 
+        </perspectiveExtension> 
+    </extension> 
+   
+   
+   <!-- VIEWS -->
+   <extension
+               point="org.eclipse.ui.views">
+               <!-- Security -->
+               <view
+                       id="org.argeo.cms.ui.workbench.usersView"
+                       class="org.argeo.eclipse.spring.SpringExtensionFactory"
+                       icon="platform:/plugin/org.argeo.cms.ui.theme/icons/person.png"
+                       name="Users"
+                       restorable="true">
+               </view>
+               <view
+                       class="org.argeo.eclipse.spring.SpringExtensionFactory"
+                       icon="platform:/plugin/org.argeo.cms.ui.theme/icons/group.png"
+                       id="org.argeo.cms.ui.workbench.groupsView"
+                       name="Groups"
+                       restorable="false">
+               </view>
+               <!-- Home -->
+               <view
+                       id="org.argeo.cms.ui.workbench.userProfile"
+                       class="org.argeo.cms.ui.workbench.useradmin.UserProfile"
+                       icon="platform:/plugin/org.argeo.cms.ui.theme/icons/person-logged-in.png"
+                       name="Profile"
+                       restorable="true">
+               </view>
+               <!-- Maintenance -->
+               <view
+                       id="org.argeo.cms.ui.workbench.logView"
+                       class="org.argeo.eclipse.spring.SpringExtensionFactory"
+                       name="Log"
+                       icon="platform:/plugin/org.argeo.cms.ui.theme/icons/log.gif"
+                       restorable="true">
+               </view>
+               <view
+            id="org.argeo.cms.ui.workbench.adminLogView"
+            class="org.argeo.eclipse.spring.SpringExtensionFactory"
+            name="Admin Log"
+            icon="platform:/plugin/org.argeo.cms.ui.theme/icons/adminLog.gif"
+            restorable="true">
+               </view>
+               <!-- OSGi Monitor -->
+               <view
+               name="Modules"
+            id="org.argeo.cms.ui.workbench.modulesView"
+            icon="platform:/plugin/org.argeo.cms.ui.theme/icons/service_published.gif"
+                       class="org.argeo.cms.ui.workbench.osgi.ModulesView">
+               </view>
+               <view
+               name="CMS Session"
+            id="org.argeo.cms.ui.workbench.cmsSessionsView"
+            icon="platform:/plugin/org.argeo.cms.ui.theme/icons/service_published.gif"
+                       class="org.argeo.cms.ui.workbench.osgi.CmsSessionsView">
+               </view>
+               <view
+               name="Bundles"
+            id="org.argeo.cms.ui.workbench.bundlesView" 
+            icon="platform:/plugin/org.argeo.cms.ui.theme/icons/bundles.gif"
+            class="org.argeo.cms.ui.workbench.osgi.BundlesView">
+               </view>
+               <view
+               name="Multiple Packages"
+            id="org.argeo.cms.ui.workbench.multiplePackagesView" 
+            icon="platform:/plugin/org.argeo.cms.ui.theme/icons/node.gif"
+            class="org.argeo.cms.ui.workbench.osgi.MultiplePackagesView">
+               </view>
+               <!-- Data Explorer -->
+               <view
+          name="JCR"
+          id="org.argeo.cms.ui.workbench.jcrBrowserView"
+          icon="platform:/plugin/org.argeo.cms.ui.theme/icons/browser.gif"
+          class="org.argeo.eclipse.spring.SpringExtensionFactory">
+          </view>
+               <view
+          name="Files"
+          id="org.argeo.cms.ui.workbench.nodeFsBrowserView"
+          icon="platform:/plugin/org.argeo.cms.ui.theme/icons/browser.gif"
+          class="org.argeo.eclipse.spring.SpringExtensionFactory">
+          </view>
+    </extension> 
+       
+       <!-- EDITORS -->
+       <extension
+               point="org.eclipse.ui.editors">
+               <!-- Security -->
+               <editor
+                       class="org.argeo.eclipse.spring.SpringExtensionFactory"
+            id="org.argeo.cms.ui.workbench.userEditor"
+            name="User"
+            icon="platform:/plugin/org.argeo.cms.ui.theme/icons/person.png"
+            default="false">
+               </editor>
+               <editor
+                       class="org.argeo.eclipse.spring.SpringExtensionFactory"
+            id="org.argeo.cms.ui.workbench.groupEditor"
+            name="User"
+            icon="platform:/plugin/org.argeo.cms.ui.theme/icons/group.png"
+            default="false">
+               </editor>
+               <!-- Data Explorer -->
+               <editor
+                       name="JCR Query"
+                       id="org.argeo.cms.ui.workbench.genericJcrQueryEditor"
+                       icon="platform:/plugin/org.argeo.cms.ui.theme/icons/query.png"
+                       class="org.argeo.eclipse.spring.SpringExtensionFactory"
+                       default="false">
+        </editor>
+               <editor
+                       name="Node Editor"
+            id="org.argeo.cms.ui.workbench.defaultNodeEditor"
+                       icon="platform:/plugin/org.argeo.cms.ui.theme/icons/query.png"
+                       class="org.argeo.eclipse.spring.SpringExtensionFactory"
+                       default="false">
+               </editor>
+       </extension>
+    
+    <extension
+         point="org.eclipse.ui.commands">
+               <!-- User CRUD -->
+               <command
+            id="org.argeo.cms.ui.workbench.newUser"
+            defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+            name="New User">
+       </command>
+               <command
+                       id="org.argeo.cms.ui.workbench.deleteUsers"
+            defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+                       name="Delete User">
+               </command>
+               <command
+               id="org.argeo.cms.ui.workbench.userBatchUpdate"
+               defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+            name="User batch update">
+               </command>
+               <!-- Group CRUD -->
+               <command
+                       id="org.argeo.cms.ui.workbench.newGroup"
+                       defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+            name="New Group">
+               </command>
+               <command
+            id="org.argeo.cms.ui.workbench.deleteGroups"
+                       defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+            name="Delete Group">
+               </command>
+               <!-- Transaction -->
+               <command
+                   id="org.argeo.cms.ui.workbench.userTransactionHandler"
+            defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+               name="Manage a user transaction">
+                       <commandParameter
+                                       id="param.commandId"
+                                       name="begin, commit or rollback">
+                       </commandParameter>
+               </command>
+               <!-- Force the refresh when the various listener are not enough -->
+               <command
+            defaultHandler="org.argeo.cms.ui.workbench.internal.useradmin.commands.ForceRefresh"
+            id="org.argeo.cms.ui.workbench.forceRefresh"
+            name="Force Refresh">
+       </command>
+               <!-- Data Explorer -->
+               <command
+                       defaultHandler="org.argeo.cms.ui.workbench.commands.OpenEditor"
+            id="org.argeo.cms.ui.workbench.openEditor"
+            name="Open an editor given its ID">
+            <commandParameter
+                               id="param.jcrNodePath"
+                               name="Node path">
+                       </commandParameter>
+            <!-- The path to the corresponding node if needed. -->
+            <commandParameter
+                               id="param.jcrNodePath"
+                               name="Node path">
+                       </commandParameter>
+               </command>
+       <command
+                       defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.GetNodeSize"
+                       id="org.argeo.cms.ui.workbench.getNodeSize"
+                       name="Get node size">
+               </command>    
+       <command
+                       defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+                       id="org.argeo.cms.ui.workbench.addRemoteRepository"
+                       name="Add remote JCR repository">
+                       <!-- <commandParameter
+                               id="param.repositoryUri"
+                               name="Repository URI">
+                       </commandParameter> -->
+               </command>    
+       <command
+                       defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.RemoveRemoteRepository"
+                       id="org.argeo.cms.ui.workbench.removeRemoteRepository"
+                       name="Remove remote JCR repository">
+               </command>    
+               <command
+               defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.AddFolderNode"
+               id="org.argeo.cms.ui.workbench.addFolderNode"
+               name="Create a new folder">
+               </command>
+               <command
+                       defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+                       id="org.argeo.cms.ui.workbench.addPrivileges"
+                       name="Add Privileges">
+               </command>
+               <command
+                       defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+                       id="org.argeo.cms.ui.workbench.removePrivileges"
+                       name="Remove Privileges">
+               </command>
+               <command
+                       defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.CreateWorkspace"
+                       id="org.argeo.cms.ui.workbench.createWorkspace"
+                       name="Create a new workspace">
+               </command>
+               <command
+                       defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.Refresh"
+                       id="org.argeo.cms.ui.workbench.refresh"
+                       name="Refresh">
+               </command>
+               <command
+                       defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.DeleteNodes"
+                       id="org.argeo.cms.ui.workbench.deleteNodes"
+                       name="Delete nodes">
+               </command>
+               <command
+               defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.UploadFiles"
+               id="org.argeo.cms.ui.workbench.uploadFiles"
+               name="Upload files">
+               </command>
+               <!-- <command
+               defaultHandler="org.argeo.eclipse.spring.SpringCommandHandler"
+               id="org.argeo.cms.ui.workbench.openFile"
+               name="Open current file">
+               </command> -->
+               <command
+               defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.DumpNode"
+               id="org.argeo.cms.ui.workbench.dumpNode"
+               name="Dump Current Selected Node">
+               </command>
+               <command
+               defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.RenameNode"
+               id="org.argeo.cms.ui.workbench.renameNode"
+               name="Rename Current Selected Node">
+               </command>
+               <command
+               defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.ConfigurableNodeDump"
+               id="org.argeo.cms.ui.workbench.nodeConfigurableDump"
+               name="Configurable dump of the selected Node">
+               </command>
+               <command
+                       defaultHandler="org.argeo.cms.ui.workbench.internal.jcr.commands.SortChildNodes"
+                       id="org.argeo.cms.ui.workbench.sortChildNodes"
+                       name="Sort node tree">
+            <!-- FIXME: default value does not work -->
+            <state 
+                               id="org.argeo.cms.ui.workbench.sortChildNodes.toggleState" 
+                               class="org.eclipse.ui.handlers.RegistryToggleState:true" >
+                               <!-- <class class="org.eclipse.jface.commands.ToggleState"> 
+                                       <parameter
+                                               name="default"
+                                       value="true" />
+                               </class> -->
+                       </state>
+               </command>
+               <!-- Utility to provide sub menues when we don't want to define a default command for this menu -->
+               <command
+                       id="org.argeo.cms.ui.workbench.doNothing"
+                       defaultHandler="org.argeo.cms.ui.workbench.commands.DoNothing"
+                       name="Open menu">
+               </command>    
+       </extension>
+       
+       <!-- MENU CONTRIBUTIONS -->
+       <extension
+               point="org.eclipse.ui.menus">
+       <!-- Main tool bar menu
+       <menuContribution locationURI="toolbar:org.eclipse.ui.main.toolbar">
+               <toolbar id="org.argeo.cms.ui.workbench.userToolbar">
+                               <command
+                                       commandId="org.argeo.cms.ui.workbench.rap.mainMenuCommand"
+                                       icon="platform:/plugin/org.argeo.cms.ui.theme/icons/home.png"
+                                       id="org.argeo.cms.ui.workbench.rap.mainMenu"
+                                       style="pulldown">
+                               </command>
+                               <command commandId="org.eclipse.ui.file.save" icon="platform:/plugin/org.argeo.cms.ui.theme/icons/save.png"/>
+                               <command commandId="org.eclipse.ui.file.saveAll" icon="platform:/plugin/org.argeo.cms.ui.theme/icons/save-all.png"/>
+                       </toolbar>
+               </menuContribution>
+               -->
+               <menuContribution
+                       locationURI="toolbar:org.argeo.cms.ui.workbench.userToolbar?after=org.eclipse.ui.file.saveAll"> 
+                       <!-- Transaction management --> 
+                       <command
+                               commandId="org.argeo.cms.ui.workbench.userTransactionHandler"
+                               icon="platform:/plugin/org.argeo.cms.ui.theme/icons/commit.gif"
+                               label="Commit Transaction"
+                               style="push"
+                               tooltip="Commit a user transaction">
+                               <parameter name="param.commandId" value="transaction.commit" />
+                               <visibleWhen>
+                                       <with variable="org.argeo.cms.ui.workbench.userTransactionState">
+                                               <equals value="status.active" />
+                                       </with>
+                               </visibleWhen>
+                       </command>
+                       <command
+                               commandId="org.argeo.cms.ui.workbench.userTransactionHandler"
+                               icon="platform:/plugin/org.argeo.cms.ui.theme/icons/rollback.gif"
+                               label="Rollback Transaction"
+                               style="push"
+                               tooltip="Abandon current changes and rollback to the latest commited version">
+                               <parameter name="param.commandId" value="transaction.rollback" />
+                               <visibleWhen>
+                                       <with variable="org.argeo.cms.ui.workbench.userTransactionState">
+                                                       <equals value="status.active" />
+                                       </with>
+                               </visibleWhen>
+                       </command>                      
+               </menuContribution>
+    
+       <!-- UsersView specific toolbar menu -->
+               <menuContribution
+            locationURI="toolbar:org.argeo.cms.ui.workbench.usersView">
+            <command
+                  commandId="org.argeo.cms.ui.workbench.deleteUsers"
+                  icon="platform:/plugin/org.argeo.cms.ui.theme/icons/delete.png"
+                  label="Delete"
+                  tooltip="Delete selected users">
+            </command>
+            <command
+                  commandId="org.argeo.cms.ui.workbench.forceRefresh"
+                  icon="platform:/plugin/org.argeo.cms.ui.theme/icons/refresh.png"
+                  label="Refresh"
+                  tooltip="Force the full refresh of the user list">
+            </command>
+            <command
+                  commandId="org.argeo.cms.ui.workbench.newUser"
+                  icon="platform:/plugin/org.argeo.cms.ui.theme/icons/add.png"
+                  label="Add"
+                  tooltip="Create a new user">
+            </command>
+            <command
+                  commandId="org.argeo.cms.ui.workbench.userBatchUpdate"
+                  icon="platform:/plugin/org.argeo.cms.ui.theme/icons/batch.gif"
+                  label="Update users"
+                  tooltip="Perform maintenance activities on a list of chosen users">
+            </command>
+        </menuContribution>
+
+       <!-- GroupsView specific toolbar menu -->
+        <menuContribution
+            locationURI="toolbar:org.argeo.cms.ui.workbench.groupsView">
+            <command
+                  commandId="org.argeo.cms.ui.workbench.deleteGroups"
+                  icon="platform:/plugin/org.argeo.cms.ui.theme/icons/delete.png"
+                  label="Delete Group"
+                  tooltip="Delete selected groups">
+            </command>
+            <command
+                  commandId="org.argeo.cms.ui.workbench.forceRefresh"
+                  icon="platform:/plugin/org.argeo.cms.ui.theme/icons/refresh.png"
+                  label="Refresh list"
+                  tooltip="Force the full refresh of the group list">
+            </command>
+            <command
+                  commandId="org.argeo.cms.ui.workbench.newGroup"
+                  icon="platform:/plugin/org.argeo.cms.ui.theme/icons/add.png"
+                  label="Add Group"
+                  tooltip="Create a new group">
+            </command>
+        </menuContribution>
+
+        <!-- Browser view specific menu --> 
+               <menuContribution
+                       locationURI="menu:org.argeo.cms.ui.workbench.jcrBrowserView">
+            <!-- See bug 149 --> 
+            <!-- <command
+               commandId="org.argeo.cms.ui.workbench.openGenericJcrQueryEditor"
+                icon="platform:/plugin/org.argeo.cms.ui.theme/icons/query.png"
+                style="push">
+            </command> --> 
+            <command
+               commandId="org.argeo.cms.ui.workbench.addRemoteRepository"
+                icon="platform:/plugin/org.argeo.cms.ui.theme/icons/addRepo.gif"
+                style="push">
+            </command>
+             <command
+               commandId="org.argeo.cms.ui.workbench.sortChildNodes"
+                icon="platform:/plugin/org.argeo.cms.ui.theme/icons/sort.gif"
+                style="toggle"
+                label="Sort child nodes"
+                tooltip="NOTE: displaying unsorted nodes will enhance overall performances">
+            </command>
+               </menuContribution>
+               <!-- Browser view popup context menu --> 
+               <menuContribution
+                       locationURI="popup:org.argeo.cms.ui.workbench.jcrBrowserView">
+                       <command
+                               commandId="org.argeo.cms.ui.workbench.refresh"
+                               icon="platform:/plugin/org.argeo.cms.ui.theme/icons/refresh.png"
+                               style="push">
+                       </command>
+                       <command
+                        commandId="org.argeo.cms.ui.workbench.addFolderNode"
+                        icon="platform:/plugin/org.argeo.cms.ui.theme/icons/addFolder.gif"
+                        label="Add Folder..."
+                        style="push">
+                               <visibleWhen>
+                                       <iterate>
+                                     <and>
+                                        <or>
+                                           <instanceof
+                                                 value="org.argeo.cms.ui.jcr.model.SingleJcrNodeElem">
+                                           </instanceof>
+                                           <instanceof
+                                                 value="org.argeo.cms.ui.jcr.model.WorkspaceElem">
+                                           </instanceof>
+                                        </or>
+                               <with variable="activeMenuSelection"><count value="1"/></with>
+                                     </and>
+                                       </iterate>
+                               </visibleWhen>
+                       </command>
+                       <command
+                        commandId="org.argeo.cms.ui.workbench.addPrivileges"
+                        icon="platform:/plugin/org.argeo.cms.ui.theme/icons/addPrivileges.gif"
+                        label="Add Privileges..."
+                        style="push">
+                               <visibleWhen>
+                                       <iterate>
+                                     <and>
+                                        <or>
+                                           <instanceof
+                                                 value="org.argeo.cms.ui.jcr.model.SingleJcrNodeElem">
+                                           </instanceof>
+                                           <instanceof
+                                                 value="org.argeo.cms.ui.jcr.model.WorkspaceElem">
+                                           </instanceof>
+                                        </or>
+                               <with variable="activeMenuSelection"><count value="1"/></with>
+                                     </and>
+                                       </iterate>
+                               </visibleWhen>
+                       </command>
+                       <command
+                        commandId="org.argeo.cms.ui.workbench.removePrivileges"
+                        icon="platform:/plugin/org.argeo.cms.ui.theme/icons/removePrivileges.gif"
+                        label="Remove Privileges..."
+                        style="push">
+                               <visibleWhen>
+                                       <iterate>
+                                     <and>
+                                        <or>
+                                           <instanceof
+                                                 value="org.argeo.cms.ui.jcr.model.SingleJcrNodeElem">
+                                           </instanceof>
+                                           <instanceof
+                                                 value="org.argeo.cms.ui.jcr.model.WorkspaceElem">
+                                           </instanceof>
+                                        </or>
+                               <with variable="activeMenuSelection"><count value="1"/></with>
+                                     </and>
+                                       </iterate>
+                               </visibleWhen>
+                       </command>
+                       <command
+                        commandId="org.argeo.cms.ui.workbench.createWorkspace"
+                        icon="platform:/plugin/org.argeo.cms.ui.theme/icons/addWorkspace.png"
+                        label="Create Workspace..."
+                        style="push">
+                               <visibleWhen>
+                                       <iterate>
+                                       <and>
+                                               <or>
+                                               <instanceof
+                                                       value="org.argeo.cms.ui.jcr.model.RepositoryElem">
+                                               </instanceof>
+                                               </or>
+                                       <with variable="activeMenuSelection"><count value="1"/></with>
+                                               </and>
+                                       </iterate>
+                               </visibleWhen>
+                       </command>
+                       <command
+                               commandId="org.argeo.cms.ui.workbench.deleteNodes"
+                               icon="platform:/plugin/org.argeo.cms.ui.theme/icons/remove.gif"
+                               label="Delete Nodes"
+                               style="push">
+                               <visibleWhen>
+                                       <iterate>
+                                               <or>
+                                                       <instanceof
+                                                               value="org.argeo.cms.ui.jcr.model.SingleJcrNodeElem" />
+                                                       <instanceof
+                                                               value="org.argeo.cms.ui.jcr.model.WorkspaceElem" />
+                                               </or>
+                                       </iterate>
+                               </visibleWhen>
+                       </command>
+                       <command
+                               commandId="org.argeo.cms.ui.workbench.uploadFiles"
+                               icon="platform:/plugin/org.argeo.cms.ui.theme/icons/import_fs.png"
+                               style="push"
+                               tooltip="Upload files from the local file sytem">
+                               <visibleWhen>
+                                       <iterate>
+                                               <and>
+                                                       <or>
+                                                               <instanceof
+                                                                       value="org.argeo.cms.ui.jcr.model.SingleJcrNodeElem" />
+                                                               <instanceof
+                                               value="org.argeo.cms.ui.jcr.model.WorkspaceElem" />
+                                       </or>
+                                       <with variable="activeMenuSelection"><count value="1"/></with>
+                                               </and>
+                                       </iterate>
+                               </visibleWhen>
+                       </command>
+                       <command
+                               commandId="org.argeo.cms.ui.workbench.addRemoteRepository"
+                               icon="platform:/plugin/org.argeo.cms.ui.theme/icons/addRepo.gif"
+                               style="push">
+                                       <visibleWhen>
+                                               <iterate> 
+                                                       <or>
+                                                               <instanceof
+                                               value="org.argeo.cms.ui.jcr.model.RepositoriesElem" />
+                                                               <instanceof
+                                                                       value="org.argeo.cms.ui.jcr.model.RepositoryElem" />
+                                                       </or> 
+                                               </iterate>
+                                       </visibleWhen>
+                       </command>
+                       <command
+                               commandId="org.argeo.cms.ui.workbench.removeRemoteRepository"
+                               icon="platform:/plugin/org.argeo.cms.ui.theme/icons/remove.gif"
+                               style="push">
+                               <visibleWhen>
+                                       <iterate> 
+                                               <or>
+                                                       <instanceof
+                                                               value="org.argeo.cms.ui.jcr.model.RemoteRepositoryElem" />
+                                               </or> 
+                               </iterate>
+                               </visibleWhen>
+                       </command>
+                       <command
+                               commandId="org.argeo.cms.ui.workbench.getNodeSize"
+                               icon="platform:/plugin/org.argeo.cms.ui.theme/icons/getSize.gif"
+                               style="push">
+                                       <visibleWhen>
+                                               <iterate>
+                                                       <or>
+                                                               <instanceof
+                                                                       value="org.argeo.cms.ui.jcr.model.SingleJcrNodeElem" />
+                                                               <instanceof
+                                                                       value="org.argeo.cms.ui.jcr.model.WorkspaceElem" />
+                                       </or>
+                                       </iterate>
+                                       </visibleWhen>
+                       </command>
+                       <command
+                       commandId="org.argeo.cms.ui.workbench.dumpNode"
+                       icon="platform:/plugin/org.argeo.cms.ui.theme/icons/dumpNode.gif"
+                               label="Dump Node"
+                               style="push">
+                               <visibleWhen>
+                                       <iterate> <and>
+                                               <instanceof value="org.argeo.cms.ui.jcr.model.SingleJcrNodeElem"></instanceof>
+                               <with variable="activeMenuSelection"><count value="1"/></with>
+                                       </and> </iterate>
+                               </visibleWhen>
+                       </command>
+                       <command
+                       commandId="org.argeo.cms.ui.workbench.renameNode"
+                       icon="platform:/plugin/org.argeo.cms.ui.theme/icons/rename.gif"
+                               label="Rename..."
+                               style="push">
+                               <visibleWhen>
+                                       <iterate> <and>
+                                               <instanceof value="org.argeo.cms.ui.jcr.model.SingleJcrNodeElem"></instanceof>
+                               <with variable="activeMenuSelection"><count value="1"/></with>
+                                       </and> </iterate>
+                               </visibleWhen>
+                       </command>
+                       <command
+                       commandId="org.argeo.cms.ui.workbench.nodeConfigurableDump"
+                       icon="platform:/plugin/org.argeo.cms.ui.theme/icons/dumpNode.gif"
+                               label="Configurable dump..."
+                               style="push">
+                               <visibleWhen>
+                                       <iterate> 
+                                               <and>
+                                                       <instanceof value="org.argeo.cms.ui.jcr.model.SingleJcrNodeElem"></instanceof>
+                                       <with variable="activeMenuSelection"><count value="1"/></with>
+                                               </and> 
+                                       </iterate>
+                               </visibleWhen>
+                       </command>
+               </menuContribution>
+        
+       </extension>
+
+       <!-- SERVICES -->
+       <extension
+       point="org.eclipse.ui.services">
+               <sourceProvider
+               id="org.argeo.cms.ui.workbench.userTransactionProvider"
+            provider="org.argeo.eclipse.spring.SpringExtensionFactory" >
+                       <variable
+                   name="org.argeo.cms.ui.workbench.userTransactionState"
+                   priorityLevel="workbench">
+               </variable>
+               </sourceProvider>
+               <sourceProvider
+              provider="org.argeo.cms.ui.workbench.util.RolesSourceProvider">
+           <variable
+                 name="org.argeo.cms.ui.workbench.rolesVariable"
+                 priorityLevel="workbench">
+           </variable>
+        </sourceProvider>
+       </extension>
+  
+       <!-- ACTIVITIES -->
+       <extension
+               point="org.eclipse.ui.activities">
+
+               <!-- Admin -->
+               <activityPatternBinding
+                       pattern="org.argeo.cms.ui.workbench/org.argeo.cms.ui.workbench.osgiPerspective"
+                       isEqualityPattern="true"
+                       activityId="org.argeo.cms.ui.workbench.adminActivity">
+               </activityPatternBinding>
+               <activityPatternBinding
+                       pattern="org.argeo.cms.ui.workbench/org.argeo.cms.ui.workbench.jcrBrowserPerspective"
+                       isEqualityPattern="true"
+                       activityId="org.argeo.cms.ui.workbench.adminActivity">
+               </activityPatternBinding>
+        <activityPatternBinding
+              pattern="org.argeo.cms.ui.workbench/org.argeo.cms.ui.workbench.adminMaintenancePerspective"
+              isEqualityPattern="true"
+              activityId="org.argeo.cms.ui.workbench.adminActivity">
+        </activityPatternBinding>
+        <activityPatternBinding
+              pattern="org.argeo.cms.ui.workbench/org.argeo.cms.ui.workbench.adminLogView"
+              isEqualityPattern="true"
+              activityId="org.argeo.cms.ui.workbench.adminActivity">
+        </activityPatternBinding>
+               
+               <!-- UserAdmin -->
+               <activityPatternBinding
+                       pattern="org.argeo.cms.ui.workbench/org.argeo.cms.ui.workbench.adminSecurityPerspective"
+                       activityId="org.argeo.cms.ui.workbench.userAdminActivity"
+                       isEqualityPattern="true">
+               </activityPatternBinding>
+
+               <!-- Users -->
+        <activityPatternBinding
+              pattern="org.argeo.cms.ui.workbench/org.argeo.cms.ui.workbench.userHomePerspective"
+              isEqualityPattern="true"
+              activityId="org.argeo.cms.ui.workbench.userActivity">
+        </activityPatternBinding>
+        <activityPatternBinding
+              pattern="org.argeo.cms.ui.workbench/org.argeo.cms.ui.workbench.userProfile"
+              isEqualityPattern="true"
+              activityId="org.argeo.cms.ui.workbench.userActivity">
+        </activityPatternBinding>
+
+
+               <!-- Activity declaration -->
+               <activity
+                       description="Authenticated users"
+                       id="org.argeo.cms.ui.workbench.userActivity"
+                       name="User">
+                       <enabledWhen>
+                               <with variable="roles">
+                                       <iterate ifEmpty="false" operator="or">
+                                               <equals value="cn=user,ou=roles,ou=node" />
+                                       </iterate>
+                               </with>
+                       </enabledWhen>
+               </activity>
+        <activity
+              description="Admins"
+              id="org.argeo.cms.ui.workbench.adminActivity"
+              name="Admin">
+                 <enabledWhen>
+                   <with variable="roles">
+                     <iterate ifEmpty="false" operator="or">
+                       <equals value="cn=admin,ou=roles,ou=node" />
+                     </iterate>
+                   </with>
+                 </enabledWhen>
+        </activity>
+        <activity
+              description="User Admins"
+              id="org.argeo.cms.ui.workbench.userAdminActivity"
+              name="User Admin">
+                 <enabledWhen>
+                   <with variable="roles">
+                     <iterate ifEmpty="false" operator="or">
+                       <equals value="cn=userAdmin,ou=roles,ou=node" />
+                     </iterate>
+                   </with>
+                 </enabledWhen>
+        </activity>
+        <activity
+              description="Non admins"
+              id="org.argeo.cms.ui.workbench.notAdminActivity"
+              name="Not Admin">
+                 <enabledWhen>
+                       <not>
+                   <with variable="roles">
+                     <iterate ifEmpty="false" operator="or">
+                       <equals value="cn=admin,ou=roles,ou=node" />
+                     </iterate>
+                   </with>
+                       </not>
+                 </enabledWhen>
+        </activity>
+        <activity
+              description="Non remote"
+              id="org.argeo.cms.ui.workbench.notRemoteActivity"
+              name="NonRemote">
+                 <enabledWhen>
+                       <not>
+                   <with variable="roles">
+                     <iterate ifEmpty="false" operator="or">
+                       <equals value="ROLE_REMOTE" />
+                     </iterate>
+                   </with>
+                       </not>
+                 </enabledWhen>
+        </activity>
+       </extension>
+       
+       <!-- STARTUP  --> 
+       <extension point="org.eclipse.ui.startup">
+               <startup class="org.argeo.cms.ui.workbench.internal.useradmin.PartStateChanged"/>
+       </extension>
+</plugin>
diff --git a/org.argeo.cms.ui.workbench/pom.xml b/org.argeo.cms.ui.workbench/pom.xml
new file mode 100644 (file)
index 0000000..8d100eb
--- /dev/null
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>argeo-commons</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.cms.ui.workbench</artifactId>
+       <name>CMS Workbench</name>
+       <packaging>jar</packaging>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <scope>provided</scope>
+               </dependency>
+
+               <dependency>
+                       <groupId>org.argeo.tp</groupId>
+                       <artifactId>argeo-tp-rap-e3</artifactId>
+                       <version>${version.argeo-tp}</version>
+                       <type>pom</type>
+                       <scope>provided</scope>
+               </dependency>
+
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/CmsWorkbenchStyles.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/CmsWorkbenchStyles.java
new file mode 100644 (file)
index 0000000..357bb68
--- /dev/null
@@ -0,0 +1,8 @@
+package org.argeo.cms.ui.workbench;
+
+/** Centralize the declaration of Workbench specific CSS Styles */
+public interface CmsWorkbenchStyles {
+
+       // Specific People layouting
+       String WORKBENCH_FORM_TEXT = "workbench_form_text";
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/JcrBrowserPerspective.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/JcrBrowserPerspective.java
new file mode 100644 (file)
index 0000000..53a916a
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench;
+
+import org.argeo.cms.ui.workbench.jcr.JcrBrowserView;
+import org.argeo.cms.ui.workbench.jcr.NodeFsBrowserView;
+import org.eclipse.ui.IFolderLayout;
+import org.eclipse.ui.IPageLayout;
+import org.eclipse.ui.IPerspectiveFactory;
+
+/** Base perspective for the Jcr browser */
+public class JcrBrowserPerspective implements IPerspectiveFactory {
+
+       public void createInitialLayout(IPageLayout layout) {
+               layout.setEditorAreaVisible(true);
+               IFolderLayout upperLeft = layout.createFolder(WorkbenchUiPlugin.PLUGIN_ID + ".upperLeft", IPageLayout.LEFT,
+                               0.4f, layout.getEditorArea());
+               upperLeft.addView(JcrBrowserView.ID);
+               upperLeft.addView(NodeFsBrowserView.ID);
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/MaintenancePerspective.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/MaintenancePerspective.java
new file mode 100644 (file)
index 0000000..9bfcbad
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench;
+
+import org.argeo.cms.ui.workbench.useradmin.AdminLogView;
+import org.argeo.cms.ui.workbench.useradmin.UserProfile;
+import org.eclipse.ui.IFolderLayout;
+import org.eclipse.ui.IPageLayout;
+import org.eclipse.ui.IPerspectiveFactory;
+
+/** First draft of a maintenance perspective. Not yet used */
+public class MaintenancePerspective implements IPerspectiveFactory {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".adminMaintenancePerspective";
+
+       public void createInitialLayout(IPageLayout layout) {
+               String editorArea = layout.getEditorArea();
+               layout.setEditorAreaVisible(true);
+               layout.setFixed(false);
+
+               IFolderLayout bottom = layout.createFolder("bottom",
+                               IPageLayout.BOTTOM, 0.50f, editorArea);
+               bottom.addView(AdminLogView.ID);
+
+               IFolderLayout left = layout.createFolder("left", IPageLayout.LEFT,
+                               0.30f, editorArea);
+               left.addView(UserProfile.ID);
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/OsgiExplorerPerspective.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/OsgiExplorerPerspective.java
new file mode 100644 (file)
index 0000000..5534a61
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench;
+
+import org.eclipse.ui.IPageLayout;
+import org.eclipse.ui.IPerspectiveFactory;
+
+/** OSGi explorer perspective (to be enriched declaratively) */
+public class OsgiExplorerPerspective implements IPerspectiveFactory {
+
+       public void createInitialLayout(IPageLayout layout) {
+               layout.setEditorAreaVisible(true);
+               layout.setFixed(false);
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/SecurityAdminPerspective.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/SecurityAdminPerspective.java
new file mode 100644 (file)
index 0000000..04b54ee
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench;
+
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.GroupsView;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UsersView;
+import org.eclipse.ui.IFolderLayout;
+import org.eclipse.ui.IPageLayout;
+import org.eclipse.ui.IPerspectiveFactory;
+
+/** Default perspective to manage users and groups */
+public class SecurityAdminPerspective implements IPerspectiveFactory {
+       public void createInitialLayout(IPageLayout layout) {
+               String editorArea = layout.getEditorArea();
+               layout.setEditorAreaVisible(true);
+               layout.setFixed(false);
+
+               IFolderLayout left = layout.createFolder("left", IPageLayout.LEFT, 0.3f, editorArea);
+               IFolderLayout right = layout.createFolder("right", IPageLayout.RIGHT, 0.5f, editorArea);
+               left.addView(UsersView.ID);
+               right.addView(GroupsView.ID);
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/UserHomePerspective.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/UserHomePerspective.java
new file mode 100644 (file)
index 0000000..e5e9817
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench;
+
+import org.argeo.cms.ui.workbench.jcr.NodeFsBrowserView;
+import org.argeo.cms.ui.workbench.useradmin.UserProfile;
+import org.eclipse.ui.IFolderLayout;
+import org.eclipse.ui.IPageLayout;
+import org.eclipse.ui.IPerspectiveFactory;
+
+/** Home perspective for the current user */
+public class UserHomePerspective implements IPerspectiveFactory {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".userHomePerspective";
+
+       public void createInitialLayout(IPageLayout layout) {
+               String editorArea = layout.getEditorArea();
+               layout.setEditorAreaVisible(true);
+               layout.setFixed(false);
+
+               IFolderLayout left = layout.createFolder("left", IPageLayout.LEFT, 0.40f, editorArea);
+               left.addView(NodeFsBrowserView.ID);
+               left.addView(UserProfile.ID);
+               // left.addView(LogView.ID);
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/WorkbenchUiPlugin.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/WorkbenchUiPlugin.java
new file mode 100644 (file)
index 0000000..d96daf2
--- /dev/null
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench;
+
+import java.io.IOException;
+import java.util.ResourceBundle;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.widgets.auth.DefaultLoginDialog;
+import org.eclipse.core.runtime.ILogListener;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.plugin.AbstractUIPlugin;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+/** The activator class controls the plug-in life cycle */
+public class WorkbenchUiPlugin extends AbstractUIPlugin implements ILogListener {
+       private final static Log log = LogFactory.getLog(WorkbenchUiPlugin.class);
+
+       // The plug-in ID
+       public final static String PLUGIN_ID = "org.argeo.cms.ui.workbench"; //$NON-NLS-1$
+       public final static String THEME_PLUGIN_ID = "org.argeo.cms.ui.theme"; //$NON-NLS-1$
+
+       private ResourceBundle messages;
+       private static BundleContext bundleContext;
+
+       public static InheritableThreadLocal<Display> display = new InheritableThreadLocal<Display>() {
+
+               @Override
+               protected Display initialValue() {
+                       return Display.getCurrent();
+               }
+       };
+
+       final static String CONTEXT_KEYRING = "KEYRING";
+
+       private CallbackHandler defaultCallbackHandler;
+       private ServiceRegistration<CallbackHandler> defaultCallbackHandlerReg;
+
+       // The shared instance
+       private static WorkbenchUiPlugin plugin;
+
+       public void start(BundleContext context) throws Exception {
+               super.start(context);
+               bundleContext = context;
+               defaultCallbackHandler = new DefaultCallbackHandler();
+               defaultCallbackHandlerReg = context.registerService(CallbackHandler.class, defaultCallbackHandler, null);
+
+               plugin = this;
+               messages = ResourceBundle.getBundle(PLUGIN_ID + ".messages");
+               Platform.addLogListener(this);
+               if (log.isTraceEnabled())
+                       log.trace("Eclipse logging now directed to standard logging");
+       }
+
+       public void stop(BundleContext context) throws Exception {
+               bundleContext = null;
+               defaultCallbackHandlerReg.unregister();
+       }
+
+       public static BundleContext getBundleContext() {
+               return bundleContext;
+       }
+
+       /*
+        * Returns the shared instance
+        * 
+        * @return the shared instance
+        */
+       public static WorkbenchUiPlugin getDefault() {
+               return plugin;
+       }
+
+       protected class DefaultCallbackHandler implements CallbackHandler {
+               public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+
+                       // if (display != null) // RCP
+                       Display displayToUse = display.get();
+                       if (displayToUse == null)// RCP
+                               displayToUse = Display.getDefault();
+                       displayToUse.syncExec(new Runnable() {
+                               public void run() {
+                                       DefaultLoginDialog dialog = new DefaultLoginDialog(display.get().getActiveShell());
+                                       try {
+                                               dialog.handle(callbacks);
+                                       } catch (IOException e) {
+                                               throw new CmsException("Cannot open dialog", e);
+                                       }
+                               }
+                       });
+                       // else {// RAP
+                       // DefaultLoginDialog dialog = new DefaultLoginDialog();
+                       // dialog.handle(callbacks);
+                       // }
+               }
+
+       }
+
+       public static ImageDescriptor getImageDescriptor(String path) {
+               return imageDescriptorFromPlugin(THEME_PLUGIN_ID, path);
+       }
+
+       /** Returns the internationalized label for the given key */
+       public static String getMessage(String key) {
+               try {
+                       return getDefault().messages.getString(key);
+               } catch (NullPointerException npe) {
+                       log.warn(key + " not found.");
+                       return key;
+               }
+       }
+
+       /**
+        * Gives access to the internationalization message bundle. Returns null in case
+        * this UiPlugin is not started (for JUnit tests, by instance)
+        */
+       public static ResourceBundle getMessagesBundle() {
+               if (getDefault() != null)
+                       // To avoid NPE
+                       return getDefault().messages;
+               else
+                       return null;
+       }
+
+       public void logging(IStatus status, String plugin) {
+               Log pluginLog = LogFactory.getLog(plugin);
+               Integer severity = status.getSeverity();
+               if (severity == IStatus.ERROR)
+                       pluginLog.error(status.getMessage(), status.getException());
+               else if (severity == IStatus.WARNING)
+                       pluginLog.warn(status.getMessage(), status.getException());
+               else if (severity == IStatus.INFO)
+                       pluginLog.info(status.getMessage(), status.getException());
+               else if (severity == IStatus.CANCEL)
+                       if (pluginLog.isDebugEnabled())
+                               pluginLog.debug(status.getMessage(), status.getException());
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/DoNothing.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/DoNothing.java
new file mode 100644 (file)
index 0000000..c8a1076
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.cms.ui.workbench.commands;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+
+/** Utilitary command to enable sub menus in various toolbars. Does nothing */
+public class DoNothing extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".doNothing";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenChangePasswordDialog.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenChangePasswordDialog.java
new file mode 100644 (file)
index 0000000..30836b9
--- /dev/null
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.commands;
+
+import static org.argeo.cms.CmsMsg.changePassword;
+import static org.argeo.cms.CmsMsg.currentPassword;
+import static org.argeo.cms.CmsMsg.newPassword;
+import static org.argeo.cms.CmsMsg.passwordChanged;
+import static org.argeo.cms.CmsMsg.repeatNewPassword;
+import static org.eclipse.jface.dialogs.IMessageProvider.INFORMATION;
+
+import java.security.AccessController;
+import java.util.Arrays;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+import javax.transaction.UserTransaction;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.node.security.CryptoKeyring;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+/** Open the change password dialog */
+public class OpenChangePasswordDialog extends AbstractHandler {
+       private final static Log log = LogFactory.getLog(OpenChangePasswordDialog.class);
+       private UserAdmin userAdmin;
+       private UserTransaction userTransaction;
+       private CryptoKeyring keyring = null;
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               ChangePasswordDialog dialog = new ChangePasswordDialog(HandlerUtil.getActiveShell(event), userAdmin);
+               if (dialog.open() == Dialog.OK) {
+                       MessageDialog.openInformation(HandlerUtil.getActiveShell(event), passwordChanged.lead(),
+                                       passwordChanged.lead());
+               }
+               return null;
+       }
+
+       @SuppressWarnings("unchecked")
+       protected void changePassword(char[] oldPassword, char[] newPassword) {
+               Subject subject = Subject.getSubject(AccessController.getContext());
+               String name = subject.getPrincipals(X500Principal.class).iterator().next().toString();
+               LdapName dn;
+               try {
+                       dn = new LdapName(name);
+               } catch (InvalidNameException e) {
+                       throw new CmsException("Invalid user dn " + name, e);
+               }
+               User user = (User) userAdmin.getRole(dn.toString());
+               if (!user.hasCredential(null, oldPassword))
+                       throw new CmsException("Invalid password");
+               if (Arrays.equals(newPassword, new char[0]))
+                       throw new CmsException("New password empty");
+               try {
+                       userTransaction.begin();
+                       user.getCredentials().put(null, newPassword);
+                       if (keyring != null) {
+                               keyring.changePassword(oldPassword, newPassword);
+                               // TODO change secret keys in the CMS session
+                       }
+                       userTransaction.commit();
+               } catch (Exception e) {
+                       try {
+                               userTransaction.rollback();
+                       } catch (Exception e1) {
+                               log.error("Could not roll back", e1);
+                       }
+                       if (e instanceof RuntimeException)
+                               throw (RuntimeException) e;
+                       else
+                               throw new CmsException("Cannot change password", e);
+               }
+       }
+
+       class ChangePasswordDialog extends TitleAreaDialog {
+               private static final long serialVersionUID = -6963970583882720962L;
+               private Text oldPassword, newPassword1, newPassword2;
+
+               public ChangePasswordDialog(Shell parentShell, UserAdmin securityService) {
+                       super(parentShell);
+               }
+
+               protected Point getInitialSize() {
+                       return new Point(400, 450);
+               }
+
+               protected Control createDialogArea(Composite parent) {
+                       Composite dialogarea = (Composite) super.createDialogArea(parent);
+                       dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       Composite composite = new Composite(dialogarea, SWT.NONE);
+                       composite.setLayout(new GridLayout(2, false));
+                       composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+                       oldPassword = createLP(composite, currentPassword.lead());
+                       newPassword1 = createLP(composite, newPassword.lead());
+                       newPassword2 = createLP(composite, repeatNewPassword.lead());
+
+                       setMessage(changePassword.lead(), INFORMATION);
+                       parent.pack();
+                       oldPassword.setFocus();
+                       return composite;
+               }
+
+               @Override
+               protected void okPressed() {
+                       try {
+                               if (!newPassword1.getText().equals(newPassword2.getText()))
+                                       throw new CmsException("New passwords are different");
+                               changePassword(oldPassword.getTextChars(), newPassword1.getTextChars());
+                               close();
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot change password", e);
+                       }
+               }
+
+               /** Creates label and password. */
+               protected Text createLP(Composite parent, String label) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.PASSWORD | SWT.BORDER);
+                       text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+                       return text;
+               }
+
+               protected void configureShell(Shell shell) {
+                       super.configureShell(shell);
+                       shell.setText(changePassword.lead());
+               }
+       }
+
+       public void setUserAdmin(UserAdmin userAdmin) {
+               this.userAdmin = userAdmin;
+       }
+
+       public void setUserTransaction(UserTransaction userTransaction) {
+               this.userTransaction = userTransaction;
+       }
+
+       public void setKeyring(CryptoKeyring keyring) {
+               this.keyring = keyring;
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenEditor.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenEditor.java
new file mode 100644 (file)
index 0000000..ecf84c3
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.commands;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.JcrQueryEditorInput;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.NodeEditorInput;
+import org.argeo.cms.ui.workbench.jcr.DefaultNodeEditor;
+import org.argeo.cms.ui.workbench.jcr.GenericJcrQueryEditor;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Open a {@link Node} editor of a specific type given the node path */
+public class OpenEditor extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".openEditor";
+
+       public final static String PARAM_PATH = "param.jcrNodePath";
+       public final static String PARAM_EDITOR_ID = "param.editorId";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               String editorId = event.getParameter(PARAM_EDITOR_ID);
+               try {
+                       IWorkbenchPage activePage = HandlerUtil.getActiveWorkbenchWindow(
+                                       event).getActivePage();
+                       if (DefaultNodeEditor.ID.equals(editorId)) {
+                               String path = event.getParameter(PARAM_PATH);
+                               NodeEditorInput nei = new NodeEditorInput(path);
+                               activePage.openEditor(nei, DefaultNodeEditor.ID);
+                       } else if (GenericJcrQueryEditor.ID.equals(editorId)) {
+                               JcrQueryEditorInput editorInput = new JcrQueryEditorInput(
+                                               GenericJcrQueryEditor.ID, null);
+                               activePage.openEditor(editorInput, editorId);
+                       }
+               } catch (PartInitException e) {
+                       throw new EclipseUiException(
+                                       "Cannot open editor of ID " + editorId, e);
+               }
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenHomePerspective.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/commands/OpenHomePerspective.java
new file mode 100644 (file)
index 0000000..0e19832
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.commands;
+
+import org.argeo.cms.ui.workbench.UserHomePerspective;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.ui.WorkbenchException;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Default action of the user menu */
+public class OpenHomePerspective extends AbstractHandler {
+       
+       public Object execute(ExecutionEvent event) throws ExecutionException {         
+               try {
+                       HandlerUtil.getActiveSite(event).getWorkbenchWindow()
+                                       .openPage(UserHomePerspective.ID, null);
+               } catch (WorkbenchException e) {
+                       ErrorFeedback.show("Cannot open home perspective", e);
+               }
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/WorkbenchConstants.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/WorkbenchConstants.java
new file mode 100644 (file)
index 0000000..8cfb0e8
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal;
+
+/** Constants used across the application. */
+@Deprecated
+public interface WorkbenchConstants {
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/AddFolderNode.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/AddFolderNode.java
new file mode 100644 (file)
index 0000000..f17fde9
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.jcr.JcrBrowserView;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.eclipse.ui.dialogs.SingleValue;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/**
+ * Adds a node of type nt:folder, only on {@link SingleJcrNodeElem} and
+ * {@link WorkspaceElem} TreeObject types.
+ * 
+ * This handler assumes that a selection provider is available and picks only
+ * first selected item. It is UI's job to enable the command only when the
+ * selection contains one and only one element. Thus no parameter is passed
+ * through the command.
+ */
+public class AddFolderNode extends AbstractHandler {
+
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".addFolderNode";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event)
+                               .getActivePage().getSelection();
+
+               JcrBrowserView view = (JcrBrowserView) HandlerUtil
+                               .getActiveWorkbenchWindow(event).getActivePage()
+                               .findView(HandlerUtil.getActivePartId(event));
+
+               if (selection != null && !selection.isEmpty()
+                               && selection instanceof IStructuredSelection) {
+                       Object obj = ((IStructuredSelection) selection).getFirstElement();
+                       TreeParent treeParentNode = null;
+                       Node jcrParentNode = null;
+
+                       if (obj instanceof SingleJcrNodeElem) {
+                               treeParentNode = (TreeParent) obj;
+                               jcrParentNode = ((SingleJcrNodeElem) treeParentNode).getNode();
+                       } else if (obj instanceof WorkspaceElem) {
+                               treeParentNode = (TreeParent) obj;
+                               jcrParentNode = ((WorkspaceElem) treeParentNode).getRootNode();
+                       } else
+                               return null;
+
+                       String folderName = SingleValue.ask("Folder name",
+                                       "Enter folder name");
+                       if (folderName != null) {
+                               try {
+                                       jcrParentNode.addNode(folderName, NodeType.NT_FOLDER);
+                                       jcrParentNode.getSession().save();
+                                       view.nodeAdded(treeParentNode);
+                               } catch (RepositoryException e) {
+                                       ErrorFeedback.show("Cannot create folder " + folderName
+                                                       + " under " + treeParentNode, e);
+                               }
+                       }
+               } else {
+                       ErrorFeedback.show(WorkbenchUiPlugin
+                                       .getMessage("errorUnvalidNtFolderNodeType"));
+               }
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/AddPrivileges.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/AddPrivileges.java
new file mode 100644 (file)
index 0000000..fafd76b
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.AddPrivilegeWizard;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.osgi.service.useradmin.UserAdmin;
+
+/** Open a dialog to add privileges on the selected node to a chosen group */
+public class AddPrivileges extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".addPrivileges";
+
+       /* DEPENDENCY INJECTION */
+       private UserAdmin userAdmin;
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event)
+                               .getActivePage().getSelection();
+               if (selection != null && !selection.isEmpty()
+                               && selection instanceof IStructuredSelection) {
+                       Object obj = ((IStructuredSelection) selection).getFirstElement();
+                       TreeParent treeParentNode = null;
+                       Node jcrParentNode = null;
+
+                       if (obj instanceof SingleJcrNodeElem) {
+                               treeParentNode = (TreeParent) obj;
+                               jcrParentNode = ((SingleJcrNodeElem) treeParentNode).getNode();
+                       } else if (obj instanceof WorkspaceElem) {
+                               treeParentNode = (TreeParent) obj;
+                               jcrParentNode = ((WorkspaceElem) treeParentNode).getRootNode();
+                       } else
+                               return null;
+
+                       try {
+                               String targetPath = jcrParentNode.getPath();
+                               AddPrivilegeWizard wizard = new AddPrivilegeWizard(
+                                               jcrParentNode.getSession(), targetPath, userAdmin);
+                               WizardDialog dialog = new WizardDialog(
+                                               HandlerUtil.getActiveShell(event), wizard);
+                               dialog.open();
+                               return null;
+                       } catch (RepositoryException re) {
+                               throw new EclipseUiException("Unable to retrieve "
+                                               + "path or JCR session to add privilege on "
+                                               + jcrParentNode, re);
+                       }
+               } else {
+                       ErrorFeedback.show("Cannot add privileges");
+               }
+               return null;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdmin(UserAdmin userAdmin) {
+               this.userAdmin = userAdmin;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/AddRemoteRepository.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/AddRemoteRepository.java
new file mode 100644 (file)
index 0000000..3539cac
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import java.net.URI;
+import java.util.Hashtable;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.cms.ui.workbench.internal.WorkbenchConstants;
+import org.argeo.cms.ui.workbench.util.CommandUtils;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeUtils;
+import org.argeo.node.security.Keyring;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Connect to a remote repository and, if successful publish it as an OSGi
+ * service.
+ */
+public class AddRemoteRepository extends AbstractHandler implements WorkbenchConstants, ArgeoNames {
+
+       private RepositoryFactory repositoryFactory;
+       private Repository nodeRepository;
+       private Keyring keyring;
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               RemoteRepositoryLoginDialog dlg = new RemoteRepositoryLoginDialog(Display.getDefault().getActiveShell());
+               if (dlg.open() == Dialog.OK) {
+                       CommandUtils.callCommand(Refresh.ID);
+               }
+               return null;
+       }
+
+       public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
+               this.repositoryFactory = repositoryFactory;
+       }
+
+       public void setKeyring(Keyring keyring) {
+               this.keyring = keyring;
+       }
+
+       public void setNodeRepository(Repository nodeRepository) {
+               this.nodeRepository = nodeRepository;
+       }
+
+       class RemoteRepositoryLoginDialog extends TitleAreaDialog {
+               private static final long serialVersionUID = 2234006887750103399L;
+               private Text name;
+               private Text uri;
+               private Text username;
+               private Text password;
+               private Button saveInKeyring;
+
+               public RemoteRepositoryLoginDialog(Shell parentShell) {
+                       super(parentShell);
+               }
+
+               protected Point getInitialSize() {
+                       return new Point(600, 400);
+               }
+
+               protected Control createDialogArea(Composite parent) {
+                       Composite dialogarea = (Composite) super.createDialogArea(parent);
+                       dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       Composite composite = new Composite(dialogarea, SWT.NONE);
+                       composite.setLayout(new GridLayout(2, false));
+                       composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+                       setMessage("Login to remote repository", IMessageProvider.NONE);
+                       name = createLT(composite, "Name", "remoteRepository");
+                       uri = createLT(composite, "URI", "http://localhost:7070/jcr/node");
+                       username = createLT(composite, "User", "");
+                       password = createLP(composite, "Password");
+
+                       saveInKeyring = createLC(composite, "Remember password", false);
+                       parent.pack();
+                       return composite;
+               }
+
+               @Override
+               protected void createButtonsForButtonBar(Composite parent) {
+                       super.createButtonsForButtonBar(parent);
+                       Button test = createButton(parent, 2, "Test", false);
+                       test.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = -1829962269440419560L;
+
+                               public void widgetSelected(SelectionEvent arg0) {
+                                       testConnection();
+                               }
+                       });
+               }
+
+               void testConnection() {
+                       Session session = null;
+                       try {
+                               URI checkedUri = new URI(uri.getText());
+                               String checkedUriStr = checkedUri.toString();
+
+                               Hashtable<String, String> params = new Hashtable<String, String>();
+                               params.put(NodeConstants.LABELED_URI, checkedUriStr);
+                               Repository repository = repositoryFactory.getRepository(params);
+                               if (username.getText().trim().equals("")) {// anonymous
+                                       // FIXME make it more generic
+                                       session = repository.login("main");
+                               } else {
+                                       // FIXME use getTextChars() when upgrading to 3.7
+                                       // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=297412
+                                       char[] pwd = password.getText().toCharArray();
+                                       SimpleCredentials sc = new SimpleCredentials(username.getText(), pwd);
+                                       session = repository.login(sc, "main");
+                                       MessageDialog.openInformation(getParentShell(), "Success",
+                                                       "Connection to '" + uri.getText() + "' successful");
+                               }
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Connection test failed for " + uri.getText(), e);
+                       } finally {
+                               JcrUtils.logoutQuietly(session);
+                       }
+               }
+
+               @Override
+               protected void okPressed() {
+                       Session nodeSession = null;
+                       try {
+                               nodeSession = nodeRepository.login();
+                               Node home = NodeUtils.getUserHome(nodeSession);
+
+                               Node remote = home.hasNode(ARGEO_REMOTE) ? home.getNode(ARGEO_REMOTE) : home.addNode(ARGEO_REMOTE);
+                               if (remote.hasNode(name.getText()))
+                                       throw new EclipseUiException("There is already a remote repository named " + name.getText());
+                               Node remoteRepository = remote.addNode(name.getText(), ArgeoTypes.ARGEO_REMOTE_REPOSITORY);
+                               remoteRepository.setProperty(ARGEO_URI, uri.getText());
+                               remoteRepository.setProperty(ARGEO_USER_ID, username.getText());
+                               nodeSession.save();
+                               if (saveInKeyring.getSelection()) {
+                                       String pwdPath = remoteRepository.getPath() + '/' + ARGEO_PASSWORD;
+                                       keyring.set(pwdPath, password.getText().toCharArray());
+                               }
+                               nodeSession.save();
+                               MessageDialog.openInformation(getParentShell(), "Repository Added",
+                                               "Remote repository '" + username.getText() + "@" + uri.getText() + "' added");
+
+                               super.okPressed();
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot add remote repository", e);
+                       } finally {
+                               JcrUtils.logoutQuietly(nodeSession);
+                       }
+               }
+
+               /** Creates label and text. */
+               protected Text createLT(Composite parent, String label, String initial) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER);
+                       text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       text.setText(initial);
+                       return text;
+               }
+
+               /** Creates label and check. */
+               protected Button createLC(Composite parent, String label, Boolean initial) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Button check = new Button(parent, SWT.CHECK);
+                       check.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       check.setSelection(initial);
+                       return check;
+               }
+
+               protected Text createLP(Composite parent, String label) {
+                       new Label(parent, SWT.NONE).setText(label);
+                       Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER | SWT.PASSWORD);
+                       text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       return text;
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/ConfigurableNodeDump.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/ConfigurableNodeDump.java
new file mode 100644 (file)
index 0000000..60f4244
--- /dev/null
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.window.Window;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/**
+ * First draft of a wizard that enable configurable recursive dump of the
+ * current selected Node (Only one at a time). Enable among other to export
+ * children Nodes and to choose to export binaries or not. It is useful to
+ * retrieve business data from live systems to prepare migration or test locally
+ */
+public class ConfigurableNodeDump extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".nodeConfigurableDump";
+
+       private final static DateFormat df = new SimpleDateFormat(
+                       "yyyy-MM-dd_HH-mm");
+
+       public final static int EXPORT_NODE = 0;
+       public final static int EXPORT_CHILDREN = 1;
+       public final static int EXPORT_GRAND_CHILDREN = 2;
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event)
+                               .getActivePage().getSelection();
+               if (selection == null || !(selection instanceof IStructuredSelection))
+                       return null;
+
+               Iterator<?> lst = ((IStructuredSelection) selection).iterator();
+               if (lst.hasNext()) {
+                       Object element = lst.next();
+                       if (element instanceof SingleJcrNodeElem) {
+                               SingleJcrNodeElem sjn = (SingleJcrNodeElem) element;
+                               Node node = sjn.getNode();
+
+                               ConfigureDumpWizard wizard = new ConfigureDumpWizard(
+                                               HandlerUtil.getActiveShell(event),
+                                               "Import Resource CSV");
+                               WizardDialog dialog = new WizardDialog(
+                                               HandlerUtil.getActiveShell(event), wizard);
+                               int result = dialog.open();
+
+                               if (result == Window.OK) {
+
+                                       String dateVal = df.format(new GregorianCalendar()
+                                                       .getTime());
+                                       try {
+
+                                               Path tmpDirPath = Files.createTempDirectory(dateVal
+                                                               + "-NodeDump-");
+                                               List<Node> toExport = retrieveToExportNodes(node,
+                                                               wizard.currExportType);
+
+                                               for (Node currNode : toExport) {
+                                                       FileOutputStream fos;
+                                                       String fileName = wizard.prefix
+                                                                       + JcrUtils.replaceInvalidChars(currNode
+                                                                                       .getName()) + "_" + dateVal
+                                                                       + ".xml";
+                                                       File currFile = new File(tmpDirPath.toString()
+                                                                       + "/" + fileName);
+                                                       currFile.createNewFile();
+                                                       fos = new FileOutputStream(currFile);
+                                                       node.getSession().exportSystemView(
+                                                                       currNode.getPath(), fos,
+                                                                       !wizard.includeBinaries, false);
+                                                       fos.flush();
+                                                       fos.close();
+                                               }
+                                       } catch (RepositoryException e) {
+                                               throw new EclipseUiException(
+                                                               "Unable to perform SystemExport on " + node, e);
+                                       } catch (IOException e) {
+                                               throw new EclipseUiException("Unable to SystemExport "
+                                                               + node, e);
+                                       }
+                               }
+                       }
+               }
+               return null;
+       }
+
+       private List<Node> retrieveToExportNodes(Node node, int currExportType)
+                       throws RepositoryException {
+               List<Node> nodes = new ArrayList<Node>();
+               switch (currExportType) {
+               case EXPORT_NODE:
+                       nodes.add(node);
+                       return nodes;
+               case EXPORT_CHILDREN:
+                       return JcrUtils.nodeIteratorToList(node.getNodes());
+               case EXPORT_GRAND_CHILDREN:
+                       NodeIterator nit = node.getNodes();
+                       while (nit.hasNext())
+                               nodes.addAll(JcrUtils.nodeIteratorToList(nit.nextNode()
+                                               .getNodes()));
+                       return nodes;
+
+               default:
+                       return nodes;
+               }
+       }
+
+       // private synchronized void openGeneratedFile(String path, String fileName)
+       // {
+       // Map<String, String> params = new HashMap<String, String>();
+       // params.put(OpenFile.PARAM_FILE_NAME, fileName);
+       // params.put(OpenFile.PARAM_FILE_URI, "file://" + path);
+       // CommandUtils.callCommand("org.argeo.security.ui.specific.openFile",
+       // params);
+       // }
+
+       private class ConfigureDumpWizard extends Wizard {
+
+               // parameters
+               protected String prefix;
+               protected int currExportType = EXPORT_NODE;
+               protected boolean includeBinaries = false;
+
+               // UI Objects
+               private BasicPage page;
+               private Text prefixTxt;
+               private Button includeBinaryBtn;
+               private Button b1, b2, b3;
+
+               public ConfigureDumpWizard(Shell parentShell, String title) {
+                       setWindowTitle(title);
+               }
+
+               @Override
+               public void addPages() {
+                       try {
+                               page = new BasicPage("Main page");
+                               addPage(page);
+                       } catch (Exception e) {
+                               throw new EclipseUiException("Cannot add page to wizard", e);
+                       }
+               }
+
+               @Override
+               public boolean performFinish() {
+                       prefix = prefixTxt.getText();
+                       if (b1.getSelection())
+                               currExportType = EXPORT_NODE;
+                       else if (b2.getSelection())
+                               currExportType = EXPORT_CHILDREN;
+                       else if (b3.getSelection())
+                               currExportType = EXPORT_GRAND_CHILDREN;
+                       includeBinaries = includeBinaryBtn.getSelection();
+                       return true;
+               }
+
+               @Override
+               public boolean performCancel() {
+                       return true;
+               }
+
+               @Override
+               public boolean canFinish() {
+                       String errorMsg = "No prefix defined.";
+                       if ("".equals(prefixTxt.getText().trim())) {
+                               page.setErrorMessage(errorMsg);
+                               return false;
+                       } else {
+                               page.setErrorMessage(null);
+                               return true;
+                       }
+               }
+
+               protected class BasicPage extends WizardPage {
+                       private static final long serialVersionUID = 1L;
+
+                       public BasicPage(String pageName) {
+                               super(pageName);
+                               setTitle("Configure dump before launching");
+                               setMessage("Define the parameters of the dump to launch");
+                       }
+
+                       public void createControl(Composite parent) {
+                               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                               // Main Layout
+                               Composite mainCmp = new Composite(parent, SWT.NONE);
+                               mainCmp.setLayout(new GridLayout(2, false));
+                               mainCmp.setLayoutData(EclipseUiUtils.fillAll());
+
+                               // The path
+                               createBoldLabel(mainCmp, "Prefix");
+                               prefixTxt = new Text(mainCmp, SWT.SINGLE | SWT.BORDER);
+                               prefixTxt.setLayoutData(EclipseUiUtils.fillAll());
+                               prefixTxt.addModifyListener(new ModifyListener() {
+                                       private static final long serialVersionUID = 1L;
+
+                                       @Override
+                                       public void modifyText(ModifyEvent event) {
+                                               if (prefixTxt.getText() != null)
+                                                       getWizard().getContainer().updateButtons();
+                                       }
+                               });
+
+                               new Label(mainCmp, SWT.SEPARATOR | SWT.HORIZONTAL)
+                                               .setLayoutData(new GridData(SWT.FILL, SWT.FILL, true,
+                                                               false, 2, 1));
+
+                               // Which node to export
+                               Label typeLbl = new Label(mainCmp, SWT.RIGHT);
+                               typeLbl.setText(" Type");
+                               typeLbl.setFont(EclipseUiUtils.getBoldFont(mainCmp));
+                               typeLbl.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false,
+                                               false, 1, 3));
+
+                               b1 = new Button(mainCmp, SWT.RADIO);
+                               b1.setText("Export this node");
+                               b1.setSelection(true);
+                               b2 = new Button(mainCmp, SWT.RADIO);
+                               b2.setText("Export children nodes");
+                               b3 = new Button(mainCmp, SWT.RADIO);
+                               b3.setText("Export grand-children nodes");
+
+                               new Label(mainCmp, SWT.SEPARATOR | SWT.HORIZONTAL)
+                                               .setLayoutData(new GridData(SWT.FILL, SWT.FILL, true,
+                                                               false, 2, 1));
+
+                               createBoldLabel(mainCmp, "Files and images");
+                               includeBinaryBtn = new Button(mainCmp, SWT.CHECK);
+                               includeBinaryBtn.setText("Include binaries");
+
+                               prefixTxt.setFocus();
+                               setControl(mainCmp);
+                       }
+               }
+       }
+
+       private Label createBoldLabel(Composite parent, String value) {
+               Label label = new Label(parent, SWT.RIGHT);
+               label.setText(" " + value);
+               label.setFont(EclipseUiUtils.getBoldFont(parent));
+               label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               return label;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/CreateWorkspace.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/CreateWorkspace.java
new file mode 100644 (file)
index 0000000..2d6949c
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import java.util.Arrays;
+
+import org.argeo.cms.ui.jcr.model.RepositoryElem;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.jcr.JcrBrowserView;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.eclipse.ui.dialogs.SingleValue;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Create a new JCR workspace */
+public class CreateWorkspace extends AbstractHandler {
+
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".addFolderNode";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event)
+                               .getActivePage().getSelection();
+
+               JcrBrowserView view = (JcrBrowserView) HandlerUtil
+                               .getActiveWorkbenchWindow(event).getActivePage()
+                               .findView(HandlerUtil.getActivePartId(event));
+
+               if (selection != null && !selection.isEmpty()
+                               && selection instanceof IStructuredSelection) {
+                       Object obj = ((IStructuredSelection) selection).getFirstElement();
+                       if (!(obj instanceof RepositoryElem))
+                               return null;
+
+                       RepositoryElem repositoryNode = (RepositoryElem) obj;
+                       String workspaceName = SingleValue.ask("Workspace name",
+                                       "Enter workspace name");
+                       if (workspaceName != null) {
+                               if (Arrays.asList(repositoryNode.getAccessibleWorkspaceNames())
+                                               .contains(workspaceName)) {
+                                       ErrorFeedback.show("Workspace " + workspaceName
+                                                       + " already exists.");
+                               } else {
+                                       repositoryNode.createWorkspace(workspaceName);
+                                       view.nodeAdded(repositoryNode);
+                               }
+                       }
+               } else {
+                       ErrorFeedback.show("Cannot create workspace");
+               }
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DeleteNodes.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DeleteNodes.java
new file mode 100644 (file)
index 0000000..a0c6770
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import java.util.Iterator;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.cms.ui.workbench.jcr.JcrBrowserView;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/**
+ * Delete the selected nodes: both in the JCR repository and in the UI view.
+ * Warning no check is done, except implementation dependent native checks,
+ * handle with care.
+ * 
+ * This handler is still 'hard linked' to a GenericJcrBrowser view to enable
+ * correct tree refresh when a node is added. This must be corrected in future
+ * versions.
+ */
+public class DeleteNodes extends AbstractHandler {
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event)
+                               .getActivePage().getSelection();
+               if (selection == null || !(selection instanceof IStructuredSelection))
+                       return null;
+
+               JcrBrowserView view = (JcrBrowserView) HandlerUtil
+                               .getActiveWorkbenchWindow(event).getActivePage()
+                               .findView(HandlerUtil.getActivePartId(event));
+
+               // confirmation
+               StringBuffer buf = new StringBuffer("");
+               Iterator<?> lst = ((IStructuredSelection) selection).iterator();
+               while (lst.hasNext()) {
+                       SingleJcrNodeElem sjn = ((SingleJcrNodeElem) lst.next());
+                       buf.append(sjn.getName()).append(' ');
+               }
+               Boolean doRemove = MessageDialog.openConfirm(
+                               HandlerUtil.getActiveShell(event), "Confirm deletion",
+                               "Do you want to delete " + buf + "?");
+
+               // operation
+               if (doRemove) {
+                       Iterator<?> it = ((IStructuredSelection) selection).iterator();
+                       Object obj = null;
+                       SingleJcrNodeElem ancestor = null;
+                       WorkspaceElem rootAncestor = null;
+                       try {
+                               while (it.hasNext()) {
+                                       obj = it.next();
+                                       if (obj instanceof SingleJcrNodeElem) {
+                                               // Cache objects
+                                               SingleJcrNodeElem sjn = (SingleJcrNodeElem) obj;
+                                               TreeParent tp = (TreeParent) sjn.getParent();
+                                               Node node = sjn.getNode();
+
+                                               // Jcr Remove
+                                               node.remove();
+                                               node.getSession().save();
+                                               // UI remove
+                                               tp.removeChild(sjn);
+
+                                               // Check if the parent is the root node
+                                               if (tp instanceof WorkspaceElem)
+                                                       rootAncestor = (WorkspaceElem) tp;
+                                               else
+                                                       ancestor = getOlder(ancestor, (SingleJcrNodeElem) tp);
+                                       }
+                               }
+                               if (rootAncestor != null)
+                                       view.nodeRemoved(rootAncestor);
+                               else if (ancestor != null)
+                                       view.nodeRemoved(ancestor);
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot delete selected node ", e);
+                       }
+               }
+               return null;
+       }
+
+       private SingleJcrNodeElem getOlder(SingleJcrNodeElem A, SingleJcrNodeElem B) {
+               try {
+                       if (A == null)
+                               return B == null ? null : B;
+                       // Todo enhanced this method
+                       else
+                               return A.getNode().getDepth() <= B.getNode().getDepth() ? A : B;
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot find ancestor", re);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DumpNode.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/DumpNode.java
new file mode 100644 (file)
index 0000000..ae23f1d
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import static org.argeo.eclipse.ui.utils.SingleSourcingConstants.FILE_SCHEME;
+import static org.argeo.eclipse.ui.utils.SingleSourcingConstants.SCHEME_HOST_SEPARATOR;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.util.CommandUtils;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.specific.OpenFile;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/**
+ * Canonically call JCR Session#exportSystemView() on the first element returned
+ * by HandlerUtil#getActiveWorkbenchWindow()
+ * (...getActivePage().getSelection()), if it is a {@link SingleJcrNodeElem},
+ * with both skipBinary and noRecurse boolean flags set to false.
+ * 
+ * Resulting stream is saved in a tmp file and opened via the {@link OpenFile}
+ * single-sourced command.
+ */
+public class DumpNode extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".dumpNode";
+
+       private final static DateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event).getActivePage().getSelection();
+               if (selection == null || !(selection instanceof IStructuredSelection))
+                       return null;
+
+               Iterator<?> lst = ((IStructuredSelection) selection).iterator();
+               if (lst.hasNext()) {
+                       Object element = lst.next();
+                       if (element instanceof SingleJcrNodeElem) {
+                               SingleJcrNodeElem sjn = (SingleJcrNodeElem) element;
+                               Node node = sjn.getNode();
+
+                               // TODO add a dialog to configure the export and ask for
+                               // confirmation
+                               // Boolean ok = MessageDialog.openConfirm(
+                               // HandlerUtil.getActiveShell(event), "Confirm dump",
+                               // "Do you want to dump " + buf + "?");
+
+                               File tmpFile;
+                               FileOutputStream fos;
+                               try {
+                                       tmpFile = File.createTempFile("JcrExport", ".xml");
+                                       tmpFile.deleteOnExit();
+                                       fos = new FileOutputStream(tmpFile);
+                                       String dateVal = df.format(new GregorianCalendar().getTime());
+                                       node.getSession().exportSystemView(node.getPath(), fos, true, false);
+                                       openGeneratedFile(tmpFile.getAbsolutePath(),
+                                                       "Dump-" + JcrUtils.replaceInvalidChars(node.getName()) + "-" + dateVal + ".xml");
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unable to perform SystemExport on " + node, e);
+                               } catch (IOException e) {
+                                       throw new EclipseUiException("Unable to SystemExport " + node, e);
+                               }
+                       }
+               }
+               return null;
+       }
+
+       private synchronized void openGeneratedFile(String path, String fileName) {
+               Map<String, String> params = new HashMap<String, String>();
+               params.put(OpenFile.PARAM_FILE_NAME, fileName);
+               params.put(OpenFile.PARAM_FILE_URI, FILE_SCHEME + SCHEME_HOST_SEPARATOR + path);
+               CommandUtils.callCommand(OpenFile.ID, params);
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/EditNode.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/EditNode.java
new file mode 100644 (file)
index 0000000..67f8238
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Property;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.ui.workbench.internal.jcr.parts.NodeEditorInput;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Generic command to open a Node in an editor. */
+public class EditNode extends AbstractHandler {
+       public final static String PARAM_EDITOR_ID = "editor";
+
+       private String defaultEditorId;
+
+       private Map<String, String> nodeTypeToEditor = new HashMap<String, String>();
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               String path = event.getParameter(Property.JCR_PATH);
+               String type = event.getParameter(NodeType.NT_NODE_TYPE);
+               if (type == null)
+                       type = NodeType.NT_UNSTRUCTURED;
+
+               String editorId = event.getParameter(PARAM_EDITOR_ID);
+               if (editorId == null)
+                       editorId = nodeTypeToEditor.containsKey(type) ? nodeTypeToEditor
+                                       .get(type) : defaultEditorId;
+
+               NodeEditorInput nei = new NodeEditorInput(path);
+               try {
+                       HandlerUtil.getActiveWorkbenchWindow(event).getActivePage()
+                                       .openEditor(nei, editorId);
+               } catch (PartInitException e) {
+                       ErrorFeedback.show("Cannot open " + editorId + " with " + path
+                                       + " of type " + type, e);
+               }
+               return null;
+       }
+
+       public void setDefaultEditorId(String defaultEditorId) {
+               this.defaultEditorId = defaultEditorId;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/GetNodeSize.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/GetNodeSize.java
new file mode 100644 (file)
index 0000000..38d6813
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Compute an approximative size for the selected node(s) */
+public class GetNodeSize extends AbstractHandler {
+       // private final static Log log = LogFactory.getLog(GetNodeSize.class);
+
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".getNodeSize";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event)
+                               .getActivePage().getSelection();
+
+               if (selection != null && !selection.isEmpty()
+                               && selection instanceof IStructuredSelection) {
+
+                       long size = 0;
+
+                       Iterator<?> it = ((IStructuredSelection) selection).iterator();
+
+                       // TODO enhance this: as the size method is recursive, we keep track
+                       // of nodes for which we already have computed size so that we don't
+                       // count them twice. In a first approximation, we assume that the
+                       // structure selection keep the nodes ordered.
+                       List<String> importedPathes = new ArrayList<String>();
+                       try {
+                               nodesIt: while (it.hasNext()) {
+                                       Object obj = it.next();
+                                       String curPath;
+                                       Node node;
+                                       if (obj instanceof SingleJcrNodeElem) {
+                                               node = ((SingleJcrNodeElem) obj).getNode();
+                                               curPath = node.getSession().getWorkspace().getName();
+                                               curPath += "/" + node.getPath();
+                                       } else if (obj instanceof WorkspaceElem) {
+                                               node = ((WorkspaceElem) obj).getRootNode();
+                                               curPath = node.getSession().getWorkspace().getName();
+                                       } else
+                                               // non valid object type
+                                               continue nodesIt;
+
+                                       Iterator<String> itPath = importedPathes.iterator();
+                                       while (itPath.hasNext()) {
+                                               String refPath = itPath.next();
+                                               if (curPath.startsWith(refPath))
+                                                       // Already done : skip node
+                                                       continue nodesIt;
+                                       }
+                                       size += JcrUtils.getNodeApproxSize(node);
+                                       importedPathes.add(curPath);
+                               }
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot Get size of selected node ", e);
+                       }
+
+                       String[] labels = { "OK" };
+                       Shell shell = HandlerUtil.getActiveWorkbenchWindow(event)
+                                       .getShell();
+                       MessageDialog md = new MessageDialog(shell, "Node size", null,
+                                       "Node size is: " + size / 1024 + " KB",
+                                       MessageDialog.INFORMATION, labels, 0);
+                       md.open();
+               }
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/Refresh.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/Refresh.java
new file mode 100644 (file)
index 0000000..1924b63
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import java.util.Iterator;
+
+import org.argeo.cms.ui.jcr.JcrBrowserUtils;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.jcr.JcrBrowserView;
+import org.argeo.eclipse.ui.TreeParent;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+
+/**
+ * Force the selected objects of the active view to be refreshed doing the
+ * following:
+ * <ol>
+ * <li>The model objects are recomputed</li>
+ * <li>the view is refreshed</li>
+ * </ol>
+ */
+public class Refresh extends AbstractHandler {
+
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".refresh";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+
+               JcrBrowserView view = (JcrBrowserView) WorkbenchUiPlugin.getDefault()
+                               .getWorkbench().getActiveWorkbenchWindow().getActivePage()
+                               .getActivePart();//
+
+               ISelection selection = WorkbenchUiPlugin.getDefault().getWorkbench()
+                               .getActiveWorkbenchWindow().getActivePage().getSelection();
+
+               if (selection != null && selection instanceof IStructuredSelection
+                               && !selection.isEmpty()) {
+                       Iterator<?> it = ((IStructuredSelection) selection).iterator();
+                       while (it.hasNext()) {
+                               Object obj = it.next();
+                               if (obj instanceof TreeParent) {
+                                       TreeParent tp = (TreeParent) obj;
+                                       JcrBrowserUtils.forceRefreshIfNeeded(tp);
+                                       view.refresh(obj);
+                               }
+                       }
+               } else if (view instanceof JcrBrowserView)
+                       ((JcrBrowserView) view).refresh(null); // force full refresh
+
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/RemovePrivileges.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/RemovePrivileges.java
new file mode 100644 (file)
index 0000000..cd2618d
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import java.security.Principal;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.security.AccessControlEntry;
+import javax.jcr.security.AccessControlList;
+import javax.jcr.security.AccessControlManager;
+import javax.jcr.security.Privilege;
+
+import org.argeo.cms.ui.jcr.JcrImages;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Open a dialog to remove privileges from the selected node */
+public class RemovePrivileges extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".removePrivileges";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event)
+                               .getActivePage().getSelection();
+               if (selection != null && !selection.isEmpty()
+                               && selection instanceof IStructuredSelection) {
+                       Object obj = ((IStructuredSelection) selection).getFirstElement();
+                       TreeParent uiNode = null;
+                       Node jcrNode = null;
+
+                       if (obj instanceof SingleJcrNodeElem) {
+                               uiNode = (TreeParent) obj;
+                               jcrNode = ((SingleJcrNodeElem) uiNode).getNode();
+                       } else if (obj instanceof WorkspaceElem) {
+                               uiNode = (TreeParent) obj;
+                               jcrNode = ((WorkspaceElem) uiNode).getRootNode();
+                       } else
+                               return null;
+
+                       try {
+                               String targetPath = jcrNode.getPath();
+                               Dialog dialog = new RemovePrivDialog(
+                                               HandlerUtil.getActiveShell(event),
+                                               jcrNode.getSession(), targetPath);
+                               dialog.open();
+                               return null;
+                       } catch (RepositoryException re) {
+                               throw new EclipseUiException("Unable to retrieve "
+                                               + "path or JCR session to add privilege on " + jcrNode,
+                                               re);
+                       }
+               } else {
+                       ErrorFeedback.show("Cannot add privileges");
+               }
+               return null;
+       }
+
+       private class RemovePrivDialog extends TitleAreaDialog {
+               private static final long serialVersionUID = 280139710002698692L;
+
+               private Composite body;
+
+               private final String path;
+               private final Session session;
+
+               public RemovePrivDialog(Shell parentShell, Session session, String path) {
+                       super(parentShell);
+                       this.session = session;
+                       this.path = path;
+               }
+
+               @Override
+               protected void configureShell(Shell newShell) {
+                       super.configureShell(newShell);
+                       newShell.setText("Remove privileges");
+               }
+
+               protected Control createDialogArea(Composite parent) {
+                       Composite dialogarea = (Composite) super.createDialogArea(parent);
+                       dialogarea.setLayoutData(new GridData(SWT.CENTER, SWT.TOP, true,
+                                       true));
+                       body = new Composite(dialogarea, SWT.NONE);
+                       body.setLayoutData(EclipseUiUtils.fillAll());
+                       refreshContent();
+                       parent.pack();
+                       return body;
+               }
+
+               private void refreshContent() {
+                       EclipseUiUtils.clear(body);
+                       try {
+                               AccessControlManager acm = session.getAccessControlManager();
+                               AccessControlList acl = JcrUtils
+                                               .getAccessControlList(acm, path);
+                               if (acl == null || acl.getAccessControlEntries().length <= 0)
+                                       setMessage("No privilege are defined on this node",
+                                                       IMessageProvider.INFORMATION);
+                               else {
+                                       body.setLayout(new GridLayout(3, false));
+                                       for (AccessControlEntry ace : acl.getAccessControlEntries()) {
+                                               addOnePrivRow(body, ace);
+                                       }
+                                       setMessage("Remove some of the defined privileges",
+                                                       IMessageProvider.INFORMATION);
+                               }
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Unable to list privileges on "
+                                               + path, e);
+                       }
+                       body.layout(true, true);
+               }
+
+               private void addOnePrivRow(Composite parent, AccessControlEntry ace) {
+                       Principal currentPrincipal = ace.getPrincipal();
+                       final String currPrincipalName = currentPrincipal.getName();
+                       new Label(parent, SWT.WRAP).setText(currPrincipalName);
+                       new Label(parent, SWT.WRAP).setText(privAsString(ace
+                                       .getPrivileges()));
+                       final Button rmBtn = new Button(parent, SWT.FLAT);
+                       rmBtn.setImage(JcrImages.REMOVE);
+
+                       rmBtn.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = 7566938841363890730L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+
+                                       if (MessageDialog.openConfirm(rmBtn.getShell(),
+                                                       "Confirm deletion",
+                                                       "Are you sure you want to remove this privilege?")) {
+                                               try {
+                                                       session.save();
+                                                       JcrUtils.clearAccessControList(session, path,
+                                                                       currPrincipalName);
+                                                       session.save();
+                                                       refreshContent();
+                                               } catch (RepositoryException re) {
+                                                       throw new EclipseUiException("Unable to "
+                                                                       + "remove privilege for "
+                                                                       + currPrincipalName + " on " + path, re);
+                                               }
+                                       }
+
+                                       super.widgetSelected(e);
+                               }
+                       });
+
+               }
+
+               private String privAsString(Privilege[] currentPrivileges) {
+
+                       StringBuilder builder = new StringBuilder();
+                       builder.append("[ ");
+                       for (Privilege priv : currentPrivileges) {
+                               builder.append(priv.getName()).append(", ");
+                       }
+                       if (builder.length() > 3)
+                               return builder.substring(0, builder.length() - 2) + " ]";
+                       else
+                               return "[]";
+
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/RemoveRemoteRepository.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/RemoveRemoteRepository.java
new file mode 100644 (file)
index 0000000..c1be6ce
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import org.argeo.cms.ui.jcr.model.RemoteRepositoryElem;
+import org.argeo.cms.ui.workbench.jcr.JcrBrowserView;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Remove a registered remote repository */
+public class RemoveRemoteRepository extends AbstractHandler {
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event)
+                               .getActivePage().getSelection();
+
+               JcrBrowserView view = (JcrBrowserView) HandlerUtil
+                               .getActiveWorkbenchWindow(event).getActivePage()
+                               .findView(HandlerUtil.getActivePartId(event));
+
+               if (selection != null && !selection.isEmpty()
+                               && selection instanceof IStructuredSelection) {
+                       Object obj = ((IStructuredSelection) selection).getFirstElement();
+
+                       if (obj instanceof RemoteRepositoryElem) {
+                               ((RemoteRepositoryElem) obj).remove();
+                               view.refresh(null);
+                       }
+               }
+               return null;
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/RenameNode.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/RenameNode.java
new file mode 100644 (file)
index 0000000..7f4b554
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import java.util.Iterator;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.jcr.JcrBrowserView;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.dialogs.SingleValue;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/**
+ * Canonically call JCR Session#move(String, String) on the first element
+ * returned by HandlerUtil#getActiveWorkbenchWindow()
+ * (...getActivePage().getSelection()), if it is a {@link SingleJcrNodeElem}.
+ * The user must then fill a new name in and confirm
+ */
+public class RenameNode extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".renameNode";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               IWorkbenchPage iwp = HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
+
+               ISelection selection = iwp.getSelection();
+               if (selection == null || !(selection instanceof IStructuredSelection))
+                       return null;
+
+               Iterator<?> lst = ((IStructuredSelection) selection).iterator();
+               if (lst.hasNext()) {
+                       Object element = lst.next();
+                       if (element instanceof SingleJcrNodeElem) {
+                               SingleJcrNodeElem sjn = (SingleJcrNodeElem) element;
+                               Node node = sjn.getNode();
+                               Session session = null;
+                               String newName = null;
+                               String oldPath = null;
+                               try {
+                                       newName = SingleValue.ask("New node name",
+                                                       "Please provide a new name for [" + node.getName() + "]");
+                                       // TODO sanity check and user feedback
+                                       newName = JcrUtils.replaceInvalidChars(newName);
+                                       oldPath = node.getPath();
+                                       session = node.getSession();
+                                       session.move(oldPath, JcrUtils.parentPath(oldPath) + "/" + newName);
+                                       session.save();
+
+                                       // Manually refresh the browser view. Must be enhanced
+                                       if (iwp.getActivePart() instanceof JcrBrowserView)
+                                               ((JcrBrowserView) iwp.getActivePart()).refresh(sjn);
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unable to rename " + node + " to " + newName, e);
+                               }
+                       }
+               }
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/SortChildNodes.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/SortChildNodes.java
new file mode 100644 (file)
index 0000000..4b3d6f3
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.jcr.JcrBrowserView;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.Command;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.commands.State;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.commands.ICommandService;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Change isSorted state of the DataExplorer Browser */
+public class SortChildNodes extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".sortChildNodes";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               JcrBrowserView view = (JcrBrowserView) HandlerUtil
+                               .getActiveWorkbenchWindow(event).getActivePage()
+                               .findView(JcrBrowserView.ID);
+
+               ICommandService service = (ICommandService) PlatformUI.getWorkbench()
+                               .getService(ICommandService.class);
+               Command command = service.getCommand(ID);
+               State state = command.getState(ID + ".toggleState");
+
+               boolean wasSorted = (Boolean) state.getValue();
+               view.setSortChildNodes(!wasSorted);
+               state.setValue(!wasSorted);
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/UploadFiles.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/commands/UploadFiles.java
new file mode 100644 (file)
index 0000000..42d4b30
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.commands;
+
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.jcr.JcrBrowserView;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Upload local file(s) under the currently selected node */
+public class UploadFiles extends AbstractHandler {
+       // private final static Log log = LogFactory.getLog(ImportFileSystem.class);
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+
+               ISelection selection = HandlerUtil.getActiveWorkbenchWindow(event).getActivePage().getSelection();
+               JcrBrowserView view = (JcrBrowserView) HandlerUtil.getActiveWorkbenchWindow(event).getActivePage()
+                               .findView(HandlerUtil.getActivePartId(event));
+               if (selection != null && !selection.isEmpty() && selection instanceof IStructuredSelection) {
+                       Object obj = ((IStructuredSelection) selection).getFirstElement();
+                       try {
+                               Node folder = null;
+                               if (obj instanceof SingleJcrNodeElem) {
+                                       folder = ((SingleJcrNodeElem) obj).getNode();
+                               } else if (obj instanceof WorkspaceElem) {
+                                       folder = ((WorkspaceElem) obj).getRootNode();
+                               } else {
+                                       ErrorFeedback.show(WorkbenchUiPlugin.getMessage("warningInvalidNodeToImport"));
+                               }
+                               if (folder != null) {
+                                       FileDialog dialog = new FileDialog(HandlerUtil.getActiveShell(event), SWT.MULTI);
+                                       dialog.setText("Choose one or more files to upload");
+
+                                       if (EclipseUiUtils.notEmpty(dialog.open())) {
+                                               String[] names = dialog.getFileNames();
+                                               // Workaround small differences between RAP and RCP
+                                               // 1. returned names are absolute path on RAP and
+                                               // relative in RCP
+                                               // 2. in RCP we must use getFilterPath that does not
+                                               // exists on RAP
+                                               Method filterMethod = null;
+                                               Path parPath = null;
+
+                                               try {
+                                                       filterMethod = dialog.getClass().getDeclaredMethod("getFilterPath");
+                                                       String filterPath = (String) filterMethod.invoke(dialog);
+                                                       parPath = Paths.get(filterPath);
+                                               } catch (NoSuchMethodException nsme) { // RAP
+                                               }
+                                               if (names.length == 0)
+                                                       return null;
+                                               else {
+                                                       loop: for (String name : names) {
+                                                               Path path = Paths.get(name);
+                                                               if (parPath != null)
+                                                                       path = parPath.resolve(path);
+                                                               if (Files.exists(path)) {
+                                                                       URI uri = path.toUri();
+                                                                       String uriStr = uri.toString();
+                                                                       System.out.println(uriStr);
+
+                                                                       if (Files.isDirectory(path)) {
+                                                                               MessageDialog.openError(HandlerUtil.getActiveShell(event),
+                                                                                               "Unimplemented directory import",
+                                                                                               "Upload of directories in the system is not yet implemented");
+                                                                               continue loop;
+                                                                       }
+                                                                       Node fileNode = folder.addNode(path.getFileName().toString(), NodeType.NT_FILE);
+                                                                       Node resNode = fileNode.addNode(Property.JCR_CONTENT, NodeType.NT_RESOURCE);
+                                                                       Binary binary = null;
+                                                                       try (InputStream is = Files.newInputStream(path)) {
+                                                                               binary = folder.getSession().getValueFactory().createBinary(is);
+                                                                               resNode.setProperty(Property.JCR_DATA, binary);
+                                                                       }
+                                                                       folder.getSession().save();
+                                                               } else {
+                                                                       String msg = "Cannot upload file at " + path.toString();
+                                                                       if (parPath != null)
+                                                                               msg += "\nPlease remember that file upload fails when choosing files from the \"Recently Used\" bookmarks on some OS";
+                                                                       MessageDialog.openError(HandlerUtil.getActiveShell(event), "Missing file", msg);
+                                                                       continue loop;
+                                                               }
+                                                       }
+                                                       view.nodeAdded((TreeParent) obj);
+                                                       return true;
+                                               }
+                                       }
+                               }
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot import files to " + obj, e);
+                       }
+               }
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/AbstractJcrQueryEditor.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/AbstractJcrQueryEditor.java
new file mode 100644 (file)
index 0000000..3839a81
--- /dev/null
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.query.QueryResult;
+import javax.jcr.query.Row;
+import javax.jcr.query.RowIterator;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.GenericTableComparator;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.EditorPart;
+
+/** Executes any JCR query. */
+public abstract class AbstractJcrQueryEditor extends EditorPart {
+       private final static Log log = LogFactory.getLog(AbstractJcrQueryEditor.class);
+
+       protected String initialQuery;
+       protected String initialQueryType;
+
+       /* DEPENDENCY INJECTION */
+       private Session session;
+
+       // Widgets
+       private TableViewer viewer;
+       private List<TableViewerColumn> tableViewerColumns = new ArrayList<TableViewerColumn>();
+       private GenericTableComparator comparator;
+
+       /** Override to layout a form enabling the end user to build his query */
+       protected abstract void createQueryForm(Composite parent);
+
+       @Override
+       public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+               JcrQueryEditorInput editorInput = (JcrQueryEditorInput) input;
+               initialQuery = editorInput.getQuery();
+               initialQueryType = editorInput.getQueryType();
+               setSite(site);
+               setInput(editorInput);
+       }
+
+       @Override
+       public final void createPartControl(final Composite parent) {
+               parent.setLayout(new FillLayout());
+
+               SashForm sashForm = new SashForm(parent, SWT.VERTICAL);
+               sashForm.setSashWidth(4);
+               sashForm.setLayout(new FillLayout());
+
+               Composite top = new Composite(sashForm, SWT.NONE);
+               GridLayout gl = new GridLayout(1, false);
+               top.setLayout(gl);
+
+               createQueryForm(top);
+
+               Composite bottom = new Composite(sashForm, SWT.NONE);
+               bottom.setLayout(new GridLayout(1, false));
+               sashForm.setWeights(getWeights());
+
+               viewer = new TableViewer(bottom);
+               viewer.getTable().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               viewer.getTable().setHeaderVisible(true);
+               viewer.setContentProvider(getQueryResultContentProvider());
+               viewer.setInput(getEditorSite());
+
+               if (getComparator() != null) {
+                       comparator = getComparator();
+                       viewer.setComparator(comparator);
+               }
+               if (getTableDoubleClickListener() != null)
+                       viewer.addDoubleClickListener(getTableDoubleClickListener());
+
+       }
+
+       protected void executeQuery(String statement) {
+               try {
+                       if (log.isDebugEnabled())
+                               log.debug("Query : " + statement);
+
+                       QueryResult qr = session.getWorkspace().getQueryManager().createQuery(statement, initialQueryType)
+                                       .execute();
+
+                       // remove previous columns
+                       for (TableViewerColumn tvc : tableViewerColumns)
+                               tvc.getColumn().dispose();
+
+                       int i = 0;
+                       for (final String columnName : qr.getColumnNames()) {
+                               TableViewerColumn tvc = new TableViewerColumn(viewer, SWT.NONE);
+                               configureColumn(columnName, tvc, i);
+                               tvc.setLabelProvider(getLabelProvider(columnName));
+                               tableViewerColumns.add(tvc);
+                               i++;
+                       }
+
+                       // Must create a local list: QueryResults can only be read once.
+                       try {
+                               List<Row> rows = new ArrayList<Row>();
+                               RowIterator rit = qr.getRows();
+                               while (rit.hasNext()) {
+                                       rows.add(rit.nextRow());
+                               }
+                               viewer.setInput(rows);
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Cannot read query result", e);
+                       }
+
+               } catch (RepositoryException e) {
+                       ErrorDialog.openError(null, "Error", "Cannot execute JCR query: " + statement,
+                                       new Status(IStatus.ERROR, "org.argeo.eclipse.ui.jcr", e.getMessage()));
+               }
+       }
+
+       /**
+        * To be overidden to adapt size of form and result frames.
+        * 
+        * @return
+        */
+       protected int[] getWeights() {
+               return new int[] { 30, 70 };
+       }
+
+       /**
+        * To be overidden to implement a doubleclick Listener on one of the rows of
+        * the table.
+        * 
+        * @return
+        */
+       protected IDoubleClickListener getTableDoubleClickListener() {
+               return null;
+       }
+
+       /**
+        * To be overiden in order to implement a specific
+        * QueryResultContentProvider
+        */
+       protected IStructuredContentProvider getQueryResultContentProvider() {
+               return new QueryResultContentProvider();
+       }
+
+       /**
+        * Enable specific implementation for columns
+        */
+       protected List<TableViewerColumn> getTableViewerColumns() {
+               return tableViewerColumns;
+       }
+
+       /**
+        * Enable specific implementation for columns
+        */
+       protected TableViewer getTableViewer() {
+               return viewer;
+       }
+
+       /**
+        * To be overridden in order to configure column label providers .
+        */
+       protected ColumnLabelProvider getLabelProvider(final String columnName) {
+               return new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -3539689333250152606L;
+
+                       public String getText(Object element) {
+                               Row row = (Row) element;
+                               try {
+                                       return row.getValue(columnName).getString();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Cannot display row " + row, e);
+                               }
+                       }
+
+                       public Image getImage(Object element) {
+                               return null;
+                       }
+               };
+       }
+
+       /**
+        * To be overridden in order to configure the columns.
+        * 
+        * @deprecated use
+        *             {@link AbstractJcrQueryEditor#configureColumn(String, TableViewerColumn , int )}
+        *             instead
+        */
+       protected void configureColumn(String jcrColumnName, TableViewerColumn column) {
+               column.getColumn().setWidth(50);
+               column.getColumn().setText(jcrColumnName);
+       }
+
+       /** To be overridden in order to configure the columns. */
+       protected void configureColumn(String jcrColumnName, TableViewerColumn column, int columnIndex) {
+               column.getColumn().setWidth(50);
+               column.getColumn().setText(jcrColumnName);
+       }
+
+       private class QueryResultContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = -5421095459600554741L;
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+
+               public Object[] getElements(Object inputElement) {
+
+                       if (inputElement instanceof List)
+                               return ((List<?>) inputElement).toArray();
+
+                       // Never reached might be deleted in future release
+                       if (!(inputElement instanceof QueryResult))
+                               return new String[] {};
+
+                       try {
+                               QueryResult queryResult = (QueryResult) inputElement;
+                               List<Row> rows = new ArrayList<Row>();
+                               RowIterator rit = queryResult.getRows();
+                               while (rit.hasNext()) {
+                                       rows.add(rit.nextRow());
+                               }
+
+                               // List<Node> elems = new ArrayList<Node>();
+                               // NodeIterator nit = queryResult.getNodes();
+                               // while (nit.hasNext()) {
+                               // elems.add(nit.nextNode());
+                               // }
+                               return rows.toArray();
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Cannot read query result", e);
+                       }
+               }
+
+       }
+
+       /**
+        * Might be used by children classes to sort columns.
+        * 
+        * @param column
+        * @param index
+        * @return
+        */
+       protected SelectionAdapter getSelectionAdapter(final TableColumn column, final int index) {
+
+               // A comparator must be define
+               if (comparator == null)
+                       return null;
+
+               SelectionAdapter selectionAdapter = new SelectionAdapter() {
+                       private static final long serialVersionUID = 239829307927778349L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+
+                               try {
+
+                                       comparator.setColumn(index);
+                                       int dir = viewer.getTable().getSortDirection();
+                                       if (viewer.getTable().getSortColumn() == column) {
+                                               dir = dir == SWT.UP ? SWT.DOWN : SWT.UP;
+                                       } else {
+
+                                               dir = SWT.DOWN;
+                                       }
+                                       viewer.getTable().setSortDirection(dir);
+                                       viewer.getTable().setSortColumn(column);
+                                       viewer.refresh();
+                               } catch (Exception exc) {
+                                       exc.printStackTrace();
+                               }
+                       }
+               };
+               return selectionAdapter;
+       }
+
+       /**
+        * To be overridden to enable sorting.
+        */
+       protected GenericTableComparator getComparator() {
+               return null;
+       }
+
+       @Override
+       public boolean isDirty() {
+               return false;
+       }
+
+       @Override
+       public void doSave(IProgressMonitor monitor) {
+               // TODO save the query in JCR?
+       }
+
+       @Override
+       public void doSaveAs() {
+       }
+
+       @Override
+       public boolean isSaveAsAllowed() {
+               return false;
+       }
+
+       /** Returns the injected current session */
+       protected Session getSession() {
+               return session;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setSession(Session session) {
+               this.session = session;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/AddPrivilegeWizard.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/AddPrivilegeWizard.java
new file mode 100644 (file)
index 0000000..6837a7d
--- /dev/null
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.security.Privilege;
+
+import org.argeo.cms.ui.useradmin.PickUpUserDialog;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+/** Add JCR privileges to the chosen user group on a given node */
+public class AddPrivilegeWizard extends Wizard {
+
+       // Context
+       private UserAdmin userAdmin;
+       private Session currentSession;
+       private String targetPath;
+       // Chosen parameters
+       private String chosenDn;
+       private User chosenUser;
+       private String jcrPrivilege;
+
+       // UI Object
+       private DefinePrivilegePage page;
+
+       // TODO enable external definition of possible values and corresponding
+       // description
+       protected static final Map<String, String> AUTH_TYPE_LABELS;
+       static {
+               Map<String, String> tmpMap = new HashMap<String, String>();
+               tmpMap.put(Privilege.JCR_READ, "jcr:read");
+               tmpMap.put(Privilege.JCR_WRITE, "jcr:write");
+               tmpMap.put(Privilege.JCR_ALL, "jcr:all");
+               AUTH_TYPE_LABELS = Collections.unmodifiableMap(tmpMap);
+       }
+
+       protected static final Map<String, String> AUTH_TYPE_DESC;
+       static {
+               Map<String, String> tmpMap = new HashMap<String, String>();
+               tmpMap.put(Privilege.JCR_READ, "The privilege to retrieve a node and get its properties and their values.");
+               tmpMap.put(Privilege.JCR_WRITE, "An aggregate privilege that "
+                               + "contains: jcr:modifyProperties, jcr:addChildNodes, " + "jcr:removeNode, jcr:removeChildNodes");
+               tmpMap.put(Privilege.JCR_ALL, "An aggregate privilege that " + "contains all JCR predefined privileges, "
+                               + "plus all implementation-defined privileges. ");
+               AUTH_TYPE_DESC = Collections.unmodifiableMap(tmpMap);
+       }
+
+       public AddPrivilegeWizard(Session currentSession, String path, UserAdmin userAdmin) {
+               super();
+               this.userAdmin = userAdmin;
+               this.currentSession = currentSession;
+               this.targetPath = path;
+       }
+
+       @Override
+       public void addPages() {
+               try {
+                       setWindowTitle("Add privilege on " + targetPath);
+                       page = new DefinePrivilegePage(userAdmin, targetPath);
+                       addPage(page);
+               } catch (Exception e) {
+                       throw new EclipseUiException("Cannot add page to wizard ", e);
+               }
+       }
+
+       @Override
+       public boolean performFinish() {
+               if (!canFinish())
+                       return false;
+               try {
+                       String username = chosenUser.getName();
+                       if (EclipseUiUtils.notEmpty(chosenDn) && chosenDn.equalsIgnoreCase(username))
+                               // Enable forcing the username case. TODO clean once this issue
+                               // has been generally addressed
+                               username = chosenDn;
+                       JcrUtils.addPrivilege(currentSession, targetPath, username, jcrPrivilege);
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException(
+                                       "Cannot set " + jcrPrivilege + " for " + chosenUser.getName() + " on " + targetPath, re);
+               }
+               return true;
+       }
+
+       private class DefinePrivilegePage extends WizardPage implements ModifyListener {
+               private static final long serialVersionUID = 8084431378762283920L;
+
+               // Context
+               final private UserAdmin userAdmin;
+
+               public DefinePrivilegePage(UserAdmin userAdmin, String path) {
+                       super("Main");
+                       this.userAdmin = userAdmin;
+                       setTitle("Define the privilege to apply to " + path);
+                       setMessage("Please choose a user or a group and relevant JCR Privilege.");
+               }
+
+               public void createControl(Composite parent) {
+                       final Composite composite = new Composite(parent, SWT.NONE);
+                       composite.setLayout(new GridLayout(3, false));
+
+                       // specify subject
+                       createBoldLabel(composite, "User or group name");
+                       final Label userNameLbl = new Label(composite, SWT.LEAD);
+                       userNameLbl.setLayoutData(EclipseUiUtils.fillWidth());
+
+                       Link pickUpLk = new Link(composite, SWT.LEFT);
+                       pickUpLk.setText(" <a>Change</a> ");
+
+                       createBoldLabel(composite, "User or group DN");
+                       final Text userNameTxt = new Text(composite, SWT.LEAD | SWT.BORDER);
+                       userNameTxt.setLayoutData(EclipseUiUtils.fillWidth(2));
+
+                       pickUpLk.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       PickUpUserDialog dialog = new PickUpUserDialog(getShell(), "Choose a group or a user", userAdmin);
+                                       if (dialog.open() == Window.OK) {
+                                               chosenUser = dialog.getSelected();
+                                               userNameLbl.setText(UserAdminUtils.getCommonName(chosenUser));
+                                               userNameTxt.setText(chosenUser.getName());
+                                       }
+                               }
+                       });
+
+                       userNameTxt.addFocusListener(new FocusListener() {
+                               private static final long serialVersionUID = 1965498600105667738L;
+
+                               @Override
+                               public void focusLost(FocusEvent event) {
+                                       String dn = userNameTxt.getText();
+                                       if (EclipseUiUtils.isEmpty(dn))
+                                               return;
+
+                                       User newChosen = null;
+                                       try {
+                                               newChosen = (User) userAdmin.getRole(dn);
+                                       } catch (Exception e) {
+                                               boolean tryAgain = MessageDialog.openQuestion(getShell(), "Unvalid DN",
+                                                               "DN " + dn + " is not valid.\nError message: " + e.getMessage()
+                                                                               + "\n\t\tDo you want to try again?");
+                                               if (tryAgain)
+                                                       userNameTxt.setFocus();
+                                               else
+                                                       resetOnFail();
+                                       }
+
+                                       if (userAdmin.getRole(dn) == null) {
+                                               boolean tryAgain = MessageDialog.openQuestion(getShell(), "Unexisting role",
+                                                               "User/group " + dn + " does not exist. " + "Do you want to try again?");
+                                               if (tryAgain)
+                                                       userNameTxt.setFocus();
+                                               else
+                                                       resetOnFail();
+                                       } else {
+                                               chosenUser = newChosen;
+                                               chosenDn = dn;
+                                               userNameLbl.setText(UserAdminUtils.getCommonName(chosenUser));
+                                       }
+                               }
+
+                               private void resetOnFail() {
+                                       String oldDn = chosenUser == null ? "" : chosenUser.getName();
+                                       userNameTxt.setText(oldDn);
+                               }
+
+                               @Override
+                               public void focusGained(FocusEvent event) {
+                               }
+                       });
+
+                       // JCR Privileges
+                       createBoldLabel(composite, "Privilege type");
+                       Combo authorizationCmb = new Combo(composite, SWT.BORDER | SWT.READ_ONLY | SWT.V_SCROLL);
+                       authorizationCmb.setItems(AUTH_TYPE_LABELS.values().toArray(new String[0]));
+                       authorizationCmb.setLayoutData(EclipseUiUtils.fillWidth(2));
+                       createBoldLabel(composite, ""); // empty cell
+                       final Label descLbl = new Label(composite, SWT.WRAP);
+                       descLbl.setLayoutData(EclipseUiUtils.fillWidth(2));
+
+                       authorizationCmb.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       String chosenPrivStr = ((Combo) e.getSource()).getText();
+                                       if (AUTH_TYPE_LABELS.containsValue(chosenPrivStr)) {
+                                               loop: for (String key : AUTH_TYPE_LABELS.keySet()) {
+                                                       if (AUTH_TYPE_LABELS.get(key).equals(chosenPrivStr)) {
+                                                               jcrPrivilege = key;
+                                                               break loop;
+                                                       }
+                                               }
+                                       }
+
+                                       if (jcrPrivilege != null) {
+                                               descLbl.setText(AUTH_TYPE_DESC.get(jcrPrivilege));
+                                               composite.layout(true, true);
+                                       }
+                               }
+                       });
+
+                       // Compulsory
+                       setControl(composite);
+               }
+
+               public void modifyText(ModifyEvent event) {
+                       String message = checkComplete();
+                       if (message != null)
+                               setMessage(message, WizardPage.ERROR);
+                       else {
+                               setMessage("Complete", WizardPage.INFORMATION);
+                               setPageComplete(true);
+                       }
+               }
+
+               /** @return error message or null if complete */
+               protected String checkComplete() {
+                       if (chosenUser == null)
+                               return "Please choose a relevant group or user";
+                       else if (userAdmin.getRole(chosenUser.getName()) == null)
+                               return "Please choose a relevant group or user";
+                       else if (jcrPrivilege == null)
+                               return "Please choose a relevant JCR privilege";
+                       return null;
+               }
+       }
+
+       private Label createBoldLabel(Composite parent, String value) {
+               Label label = new Label(parent, SWT.RIGHT);
+               label.setText(" " + value);
+               label.setFont(EclipseUiUtils.getBoldFont(parent));
+               label.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               return label;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/ChildNodesPage.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/ChildNodesPage.java
new file mode 100644 (file)
index 0000000..eb86292
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.ui.jcr.JcrTreeContentProvider;
+import org.argeo.cms.ui.jcr.NodeLabelProvider;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.jcr.DefaultNodeEditor;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/** List all children of the current node */
+public class ChildNodesPage extends FormPage {
+       // private final static Log log = LogFactory.getLog(ChildNodesPage.class);
+
+       private Node currentNode;
+
+       private JcrTreeContentProvider nodeContentProvider;
+       private TreeViewer nodesViewer;
+
+       public ChildNodesPage(FormEditor editor, String title, Node currentNode) {
+               super(editor, "ChildNodesPage", title);
+               this.currentNode = currentNode;
+       }
+
+       protected void createFormContent(IManagedForm managedForm) {
+               try {
+                       ScrolledForm form = managedForm.getForm();
+                       form.setText(WorkbenchUiPlugin.getMessage("childNodesPageTitle"));
+                       Composite innerBox = form.getBody();
+                       // Composite innerBox = new Composite(body, SWT.NO_FOCUS);
+                       GridLayout twt = new GridLayout(1, false);
+                       twt.marginWidth = twt.marginHeight = 5;
+                       innerBox.setLayout(twt);
+                       if (!currentNode.hasNodes()) {
+                               managedForm.getToolkit().createLabel(innerBox, WorkbenchUiPlugin.getMessage("warningNoChildNode"));
+                       } else {
+                               nodeContentProvider = new JcrTreeContentProvider();
+                               nodesViewer = createNodeViewer(innerBox, nodeContentProvider);
+                               nodesViewer.setInput(currentNode);
+                       }
+               } catch (Exception e) {
+                       throw new EclipseUiException("Cannot create children page for " + currentNode, e);
+               }
+       }
+
+       protected TreeViewer createNodeViewer(Composite parent, final ITreeContentProvider nodeContentProvider) {
+
+               final TreeViewer tmpNodeViewer = new TreeViewer(parent, SWT.BORDER);
+               Tree tree = tmpNodeViewer.getTree();
+               tree.setLinesVisible(true);
+               tmpNodeViewer.getTree().setLayoutData(EclipseUiUtils.fillAll());
+               tmpNodeViewer.setContentProvider(nodeContentProvider);
+               tmpNodeViewer.setLabelProvider(new NodeLabelProvider());
+               tmpNodeViewer.addDoubleClickListener(new DClickListener());
+               return tmpNodeViewer;
+       }
+
+       public class DClickListener implements IDoubleClickListener {
+
+               public void doubleClick(DoubleClickEvent event) {
+                       if (event.getSelection() == null || event.getSelection().isEmpty())
+                               return;
+                       Object obj = ((IStructuredSelection) event.getSelection()).getFirstElement();
+                       if (obj instanceof Node) {
+                               Node node = (Node) obj;
+                               try {
+                                       GenericNodeEditorInput gnei = new GenericNodeEditorInput(node);
+                                       WorkbenchUiPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getActivePage()
+                                                       .openEditor(gnei, DefaultNodeEditor.ID);
+                               } catch (PartInitException pie) {
+                                       throw new EclipseUiException("Cannot open editor for " + node, pie);
+                               }
+                       }
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/ChooseNameDialog.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/ChooseNameDialog.java
new file mode 100644 (file)
index 0000000..080ea94
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/** Ask end user for a name */
+public class ChooseNameDialog extends TitleAreaDialog {
+       private static final long serialVersionUID = 280139710002698692L;
+       private Text nameTxt;
+
+       public ChooseNameDialog(Shell parentShell) {
+               super(parentShell);
+               setTitle("Choose name");
+       }
+
+       protected Point getInitialSize() {
+               return new Point(300, 250);
+       }
+
+       protected Control createDialogArea(Composite parent) {
+               Composite dialogarea = (Composite) super.createDialogArea(parent);
+               dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               Composite composite = new Composite(dialogarea, SWT.NONE);
+               composite.setLayout(new GridLayout(2, false));
+               composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               nameTxt = createLT(composite, "Name");
+               setMessage("Choose name", IMessageProvider.INFORMATION);
+               parent.pack();
+               nameTxt.setFocus();
+               return composite;
+       }
+
+       /** Creates label and text. */
+       protected Text createLT(Composite parent, String label) {
+               new Label(parent, SWT.NONE).setText(label);
+               Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER);
+               text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               return text;
+       }
+
+       public String getName() {
+               return nameTxt.getText();
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/GenericNodeEditorInput.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/GenericNodeEditorInput.java
new file mode 100644 (file)
index 0000000..4386694
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IPersistableElement;
+
+/** Editor input for {@link Node} editors */
+public class GenericNodeEditorInput implements IEditorInput {
+       private final Node currentNode;
+
+       // Caches key properties at creation time to avoid Exception at recovering
+       // time when the session has been closed
+       private String path;
+       private String uid;
+       private String name;
+
+       public GenericNodeEditorInput(Node currentNode) {
+               this.currentNode = currentNode;
+               try {
+                       name = currentNode.getName();
+                       uid = currentNode.getIdentifier();
+                       path = currentNode.getPath();
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot cache the key properties for " + currentNode, re);
+               }
+       }
+
+       public Node getCurrentNode() {
+               return currentNode;
+       }
+
+       @SuppressWarnings("unchecked")
+       public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
+               return null;
+       }
+
+       public boolean exists() {
+               return true;
+       }
+
+       public ImageDescriptor getImageDescriptor() {
+               return null;
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public String getUid() {
+               return uid;
+       }
+
+       public String getToolTipText() {
+               return path;
+       }
+
+       public String getPath() {
+               return path;
+       }
+
+       public IPersistableElement getPersistable() {
+               return null;
+       }
+
+       /**
+        * Equals method based on UID that is unique within a workspace and path of
+        * the node, thus 2 shared node that have same UID as defined in the spec
+        * but 2 different paths will open two distinct editors.
+        * 
+        * TODO enhance this method to support multi repository and multi workspace
+        * environments
+        */
+       public boolean equals(Object obj) {
+               if (this == obj)
+                       return true;
+               if (obj == null)
+                       return false;
+               if (getClass() != obj.getClass())
+                       return false;
+
+               GenericNodeEditorInput other = (GenericNodeEditorInput) obj;
+               if (!getUid().equals(other.getUid()))
+                       return false;
+               if (!getPath().equals(other.getPath()))
+                       return false;
+               return true;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/GenericNodePage.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/GenericNodePage.java
new file mode 100644 (file)
index 0000000..2f2ec89
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ListIterator;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.CmsConstants;
+import org.argeo.cms.ui.workbench.internal.WorkbenchConstants;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.AbstractFormPart;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Work-In-Progress Node editor page: provides edition feature on String
+ * properties for power users. TODO implement manual modification of all
+ * property types.
+ */
+
+public class GenericNodePage extends FormPage implements WorkbenchConstants {
+       // private final static Log log = LogFactory.getLog(GenericNodePage.class);
+
+       // local constants
+       private final static String JCR_PROPERTY_NAME = "jcr:name";
+
+       // Utils
+       protected DateFormat timeFormatter = new SimpleDateFormat(CmsConstants.DATE_TIME_FORMAT);
+
+       // Main business Objects
+       private Node currentNode;
+
+       // This page widgets
+       private FormToolkit tk;
+       private List<Control> modifyableProperties = new ArrayList<Control>();
+
+       public GenericNodePage(FormEditor editor, String title, Node currentNode) {
+               super(editor, "id", title);
+               this.currentNode = currentNode;
+       }
+
+       protected void createFormContent(IManagedForm managedForm) {
+               tk = managedForm.getToolkit();
+               ScrolledForm form = managedForm.getForm();
+               Composite innerBox = form.getBody();
+               // Composite innerBox = new Composite(form.getBody(), SWT.NO_FOCUS);
+               GridLayout twt = new GridLayout(3, false);
+               innerBox.setLayout(twt);
+               createPropertiesPart(innerBox);
+       }
+
+       private void createPropertiesPart(Composite parent) {
+               try {
+                       AbstractFormPart part = new AbstractFormPart() {
+                               public void commit(boolean onSave) {
+                                       try {
+                                               if (onSave) {
+                                                       ListIterator<Control> it = modifyableProperties.listIterator();
+                                                       while (it.hasNext()) {
+                                                               // we only support Text controls
+                                                               Text curControl = (Text) it.next();
+                                                               String value = curControl.getText();
+                                                               currentNode.setProperty((String) curControl.getData(JCR_PROPERTY_NAME), value);
+                                                       }
+
+                                                       // We only commit when onSave = true,
+                                                       // thus it is still possible to save after a tab
+                                                       // change.
+                                                       if (currentNode.getSession().hasPendingChanges())
+                                                               currentNode.getSession().save();
+                                                       super.commit(onSave);
+                                               }
+                                       } catch (RepositoryException re) {
+                                               throw new EclipseUiException("Cannot save properties on " + currentNode, re);
+                                       }
+                               }
+                       };
+
+                       PropertyIterator pi = currentNode.getProperties();
+                       while (pi.hasNext()) {
+                               Property prop = pi.nextProperty();
+                               addPropertyLine(parent, part, prop);
+                       }
+                       getManagedForm().addPart(part);
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot display properties for " + currentNode, re);
+               }
+       }
+
+       private void addPropertyLine(Composite parent, AbstractFormPart part, Property prop) {
+               try {
+                       tk.createLabel(parent, prop.getName());
+                       tk.createLabel(parent, "[" + JcrUtils.getPropertyDefinitionAsString(prop) + "]");
+
+                       if (prop.getDefinition().isProtected()) {
+                               tk.createLabel(parent, formatReadOnlyPropertyValue(prop));
+                       } else
+                               addModifyableValueWidget(parent, part, prop);
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot display property " + prop, re);
+               }
+       }
+
+       private String formatReadOnlyPropertyValue(Property prop) throws RepositoryException {
+               String strValue;
+               if (prop.getType() == PropertyType.BINARY)
+                       strValue = "<binary>";
+               else if (prop.isMultiple())
+                       strValue = Arrays.asList(prop.getValues()).toString();
+               else if (prop.getType() == PropertyType.DATE)
+                       strValue = timeFormatter.format(prop.getValue().getDate().getTime());
+               else
+                       strValue = prop.getValue().getString();
+               return strValue;
+       }
+
+       private Control addModifyableValueWidget(Composite parent, AbstractFormPart part, Property prop)
+                       throws RepositoryException {
+               GridData gd;
+               if (prop.getType() == PropertyType.STRING && !prop.isMultiple()) {
+                       Text txt = tk.createText(parent, prop.getString(), SWT.WRAP | SWT.MULTI);
+                       gd = new GridData(GridData.FILL_HORIZONTAL);
+                       txt.setLayoutData(gd);
+                       txt.addModifyListener(new ModifiedFieldListener(part));
+                       txt.setData(JCR_PROPERTY_NAME, prop.getName());
+                       modifyableProperties.add(txt);
+               } else {
+                       // unsupported property type for editing, we create a read only
+                       // label.
+                       return tk.createLabel(parent, formatReadOnlyPropertyValue(prop));
+               }
+               return null;
+       }
+
+       private class ModifiedFieldListener implements ModifyListener {
+               private static final long serialVersionUID = 2117484480773434646L;
+               private AbstractFormPart formPart;
+
+               public ModifiedFieldListener(AbstractFormPart generalPart) {
+                       this.formPart = generalPart;
+               }
+
+               public void modifyText(ModifyEvent e) {
+                       formPart.markDirty();
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/GenericPropertyPage.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/GenericPropertyPage.java
new file mode 100644 (file)
index 0000000..50f8962
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.jcr.PropertyLabelProvider;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.WorkbenchConstants;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.layout.TreeColumnLayout;
+import org.eclipse.jface.viewers.ColumnWeightData;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Generic editor property page. Lists all properties of current node as a
+ * complex tree. TODO: enable editing
+ */
+public class GenericPropertyPage extends FormPage implements WorkbenchConstants {
+       // private final static Log log =
+       // LogFactory.getLog(GenericPropertyPage.class);
+
+       // Main business Objects
+       private Node currentNode;
+
+       public GenericPropertyPage(FormEditor editor, String title, Node currentNode) {
+               super(editor, "id", title);
+               this.currentNode = currentNode;
+       }
+
+       protected void createFormContent(IManagedForm managedForm) {
+               ScrolledForm form = managedForm.getForm();
+               form.setText(WorkbenchUiPlugin.getMessage("genericNodePageTitle"));
+               Composite innerBox = form.getBody();
+               //Composite innerBox = new Composite(body, SWT.NO_FOCUS);
+               FillLayout layout = new FillLayout();
+               layout.marginHeight = 5;
+               layout.marginWidth = 5;
+               innerBox.setLayout(layout);
+               createComplexTree(innerBox);
+               // TODO TreeColumnLayout triggers a scroll issue with the form:
+               // The inside body is always to big and a scroll bar is shown
+               // Composite tableCmp = new Composite(body, SWT.NO_FOCUS);
+               // createComplexTree(tableCmp);
+       }
+
+       private TreeViewer createComplexTree(Composite parent) {
+               int style = SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION;
+               Tree tree = new Tree(parent, style);
+               TreeColumnLayout tableColumnLayout = new TreeColumnLayout();
+
+               createColumn(tree, tableColumnLayout, "Property", SWT.LEFT, 200, 30);
+               createColumn(tree, tableColumnLayout, "Value(s)", SWT.LEFT, 300, 60);
+               createColumn(tree, tableColumnLayout, "Type", SWT.LEFT, 75, 10);
+               createColumn(tree, tableColumnLayout, "Attributes", SWT.LEFT, 75, 0);
+               // Do not apply the treeColumnLayout it does not work yet
+               // parent.setLayout(tableColumnLayout);
+
+               tree.setLinesVisible(true);
+               tree.setHeaderVisible(true);
+
+               TreeViewer treeViewer = new TreeViewer(tree);
+               treeViewer.setContentProvider(new TreeContentProvider());
+               treeViewer.setLabelProvider(new PropertyLabelProvider());
+               treeViewer.setInput(currentNode);
+               treeViewer.expandAll();
+               return treeViewer;
+       }
+
+       private static TreeColumn createColumn(Tree parent, TreeColumnLayout tableColumnLayout, String name, int style,
+                       int width, int weight) {
+               TreeColumn column = new TreeColumn(parent, style);
+               column.setText(name);
+               column.setWidth(width);
+               column.setMoveable(true);
+               column.setResizable(true);
+               tableColumnLayout.setColumnData(column, new ColumnWeightData(weight, width, true));
+               return column;
+       }
+
+       private class TreeContentProvider implements ITreeContentProvider {
+               private static final long serialVersionUID = -6162736530019406214L;
+
+               public Object[] getElements(Object parent) {
+                       Object[] props = null;
+                       try {
+
+                               if (parent instanceof Node) {
+                                       Node node = (Node) parent;
+                                       PropertyIterator pi;
+                                       pi = node.getProperties();
+                                       List<Property> propList = new ArrayList<Property>();
+                                       while (pi.hasNext()) {
+                                               propList.add(pi.nextProperty());
+                                       }
+                                       props = propList.toArray();
+                               }
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Unexpected exception while listing node properties", e);
+                       }
+                       return props;
+               }
+
+               public Object getParent(Object child) {
+                       return null;
+               }
+
+               public Object[] getChildren(Object parent) {
+                       if (parent instanceof Property) {
+                               Property prop = (Property) parent;
+                               try {
+                                       if (prop.isMultiple())
+                                               return prop.getValues();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Cannot get multi-prop values on " + prop, e);
+                               }
+                       }
+                       return null;
+               }
+
+               public boolean hasChildren(Object parent) {
+                       try {
+                               return (parent instanceof Property && ((Property) parent).isMultiple());
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Cannot check if property is multiple for " + parent, e);
+                       }
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+
+               public void dispose() {
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/JcrQueryEditorInput.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/JcrQueryEditorInput.java
new file mode 100644 (file)
index 0000000..11256fe
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import javax.jcr.query.Query;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IPersistableElement;
+
+public class JcrQueryEditorInput implements IEditorInput {
+       private final String query;
+       private final String queryType;
+
+       public JcrQueryEditorInput(String query, String queryType) {
+               this.query = query;
+               if (queryType == null)
+                       this.queryType = Query.JCR_SQL2;
+               else
+                       this.queryType = queryType;
+       }
+
+       @SuppressWarnings("unchecked")
+       public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
+               return null;
+       }
+
+       public boolean exists() {
+               return true;
+       }
+
+       public ImageDescriptor getImageDescriptor() {
+               return null;
+       }
+
+       public String getName() {
+               return query;
+       }
+
+       public IPersistableElement getPersistable() {
+               return null;
+       }
+
+       public String getToolTipText() {
+               return query;
+       }
+
+       public String getQuery() {
+               return query;
+       }
+
+       public String getQueryType() {
+               return queryType;
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/NodeEditorInput.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/NodeEditorInput.java
new file mode 100644 (file)
index 0000000..d51aeb3
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IPersistableElement;
+
+/**
+ * A canonical editor input based on a path to a node. In a multirepository
+ * environment, path can be enriched with Repository Alias and workspace
+ */
+
+public class NodeEditorInput implements IEditorInput {
+       private final String path;
+
+       public NodeEditorInput(String path) {
+               this.path = path;
+       }
+
+       @SuppressWarnings("unchecked")
+       public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
+               return null;
+       }
+
+       public boolean exists() {
+               return true;
+       }
+
+       public ImageDescriptor getImageDescriptor() {
+               return null;
+       }
+
+       public String getName() {
+               return path;
+       }
+
+       public IPersistableElement getPersistable() {
+               return null;
+       }
+
+       public String getToolTipText() {
+               return path;
+       }
+
+       public String getPath() {
+               return path;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/NodePrivilegesPage.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/NodePrivilegesPage.java
new file mode 100644 (file)
index 0000000..5aee1f3
--- /dev/null
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Display and edit a given node privilege. For the time being it is completely
+ * JackRabbit specific (and hard coded for this) and will display an empty page
+ * if using any other implementation
+ */
+public class NodePrivilegesPage extends FormPage {
+
+       private Node context;
+
+       private TableViewer viewer;
+
+       public NodePrivilegesPage(FormEditor editor, String title, Node context) {
+               super(editor, "NodePrivilegesPage", title);
+               this.context = context;
+       }
+
+       protected void createFormContent(IManagedForm managedForm) {
+               ScrolledForm form = managedForm.getForm();
+               form.setText(WorkbenchUiPlugin.getMessage("nodeRightsManagementPageTitle"));
+               FillLayout layout = new FillLayout();
+               layout.marginHeight = 5;
+               layout.marginWidth = 5;
+               form.getBody().setLayout(layout);
+               if (isJackRabbit())
+                       createRightsPart(form.getBody());
+       }
+
+       /** Creates the authorization part */
+       protected void createRightsPart(Composite parent) {
+               Table table = new Table(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+               table.setLinesVisible(true);
+               table.setHeaderVisible(true);
+               viewer = new TableViewer(table);
+
+               // Group / user name
+               TableViewerColumn column = createTableViewerColumn(viewer, "User/Group Name", 280);
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -2290781173498395973L;
+
+                       public String getText(Object element) {
+                               Node node = (Node) element;
+                               try {
+                                       if (node.hasProperty("rep:principalName"))
+                                               return node.getProperty("rep:principalName").getString();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unable to retrieve " + "principal name on " + node, e);
+                               }
+                               return "";
+                       }
+
+                       public Image getImage(Object element) {
+                               return null;
+                       }
+               });
+
+               // Privileges
+               column = createTableViewerColumn(viewer, "Assigned privileges", 300);
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -2290781173498395973L;
+                       private String propertyName = "rep:privileges";
+
+                       public String getText(Object element) {
+                               Node node = (Node) element;
+                               try {
+                                       if (node.hasProperty(propertyName)) {
+                                               String separator = ", ";
+                                               Value[] langs = node.getProperty(propertyName).getValues();
+                                               StringBuilder builder = new StringBuilder();
+                                               for (Value val : langs) {
+                                                       String currStr = val.getString();
+                                                       builder.append(currStr).append(separator);
+                                               }
+                                               if (builder.lastIndexOf(separator) >= 0)
+                                                       return builder.substring(0, builder.length() - separator.length());
+                                               else
+                                                       return builder.toString();
+
+                                       }
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unable to retrieve " + "privileges on " + node, e);
+                               }
+                               return "";
+                       }
+
+                       public Image getImage(Object element) {
+                               return null;
+                       }
+               });
+
+               // Relevant node
+               column = createTableViewerColumn(viewer, "Relevant node", 300);
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = 4245522992038244849L;
+
+                       public String getText(Object element) {
+                               Node node = (Node) element;
+                               try {
+                                       return node.getParent().getParent().getPath();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unable get path for " + node, e);
+                               }
+                       }
+
+                       public Image getImage(Object element) {
+                               return null;
+                       }
+               });
+
+               viewer.setContentProvider(new RightsContentProvider());
+               viewer.setInput(getEditorSite());
+       }
+
+       protected TableViewerColumn createTableViewerColumn(TableViewer viewer, String title, int bound) {
+               TableViewerColumn viewerColumn = new TableViewerColumn(viewer, SWT.NONE);
+               TableColumn column = viewerColumn.getColumn();
+               column.setText(title);
+               column.setWidth(bound);
+               column.setResizable(true);
+               column.setMoveable(true);
+               return viewerColumn;
+       }
+
+       private class RightsContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = -7631476348552802706L;
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+
+               // TODO JackRabbit specific retrieval of authorization. Clean and
+               // generalize
+               public Object[] getElements(Object inputElement) {
+                       try {
+                               List<Node> privs = new ArrayList<Node>();
+
+                               Node currNode = context;
+                               String currPath = currNode.getPath();
+
+                               loop: while (true) {
+                                       if (currNode.hasNode("rep:policy")) {
+                                               NodeIterator nit = currNode.getNode("rep:policy").getNodes();
+                                               while (nit.hasNext()) {
+                                                       Node currPrivNode = nit.nextNode();
+                                                       if (currPrivNode.getName().startsWith("allow"))
+                                                               privs.add(currPrivNode);
+                                               }
+                                       }
+                                       if ("/".equals(currPath))
+                                               break loop;
+                                       else {
+                                               currNode = currNode.getParent();
+                                               currPath = currNode.getPath();
+                                       }
+                               }
+
+                               // AccessControlManager acm = context.getSession()
+                               // .getAccessControlManager();
+                               // AccessControlPolicyIterator acpi = acm
+                               // .getApplicablePolicies(context.getPath());
+                               //
+                               // List<AccessControlPolicy> acps = new
+                               // ArrayList<AccessControlPolicy>();
+                               // try {
+                               // while (true) {
+                               // Object obj = acpi.next();
+                               // acps.add((AccessControlPolicy) obj);
+                               // }
+                               // } catch (Exception e) {
+                               // // No more elements
+                               // }
+                               //
+                               // AccessControlList acl = ((AccessControlList) acps.get(0));
+                               // AccessControlEntry[] entries = acl.getAccessControlEntries();
+
+                               return privs.toArray();
+                       } catch (Exception e) {
+                               throw new EclipseUiException("Cannot retrieve authorization for " + context, e);
+                       }
+               }
+       }
+
+       /**
+        * Simply checks if we are using jackrabbit without adding code dependencies
+        */
+       private boolean isJackRabbit() {
+               try {
+                       String cname = context.getSession().getClass().getName();
+                       return cname.startsWith("org.apache.jackrabbit");
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot check JCR implementation used on " + context, e);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/NodeVersionHistoryPage.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/NodeVersionHistoryPage.java
new file mode 100644 (file)
index 0000000..166ece9
--- /dev/null
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.version.Version;
+import javax.jcr.version.VersionHistory;
+import javax.jcr.version.VersionIterator;
+import javax.jcr.version.VersionManager;
+
+import org.argeo.cms.ui.CmsConstants;
+import org.argeo.cms.ui.jcr.FullVersioningTreeContentProvider;
+import org.argeo.cms.ui.jcr.JcrDClickListener;
+import org.argeo.cms.ui.jcr.VersionLabelProvider;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.WorkbenchConstants;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.PropertyDiff;
+import org.argeo.jcr.VersionDiff;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.AbstractFormPart;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.forms.widgets.TableWrapLayout;
+
+/**
+ * Offers two main sections : one to display a text area with a summary of all
+ * variations between a version and its predecessor and one tree view that
+ * enable browsing
+ */
+public class NodeVersionHistoryPage extends FormPage implements WorkbenchConstants {
+       // private final static Log log = LogFactory
+       // .getLog(NodeVersionHistoryPage.class);
+
+       // Utils
+       protected DateFormat timeFormatter = new SimpleDateFormat(CmsConstants.DATE_TIME_FORMAT);
+
+       // business objects
+       private Node currentNode;
+
+       // this page UI components
+       private FullVersioningTreeContentProvider nodeContentProvider;
+       private TreeViewer nodesViewer;
+       private FormToolkit tk;
+
+       public NodeVersionHistoryPage(FormEditor editor, String title, Node currentNode) {
+               super(editor, "NodeVersionHistoryPage", title);
+               this.currentNode = currentNode;
+       }
+
+       protected void createFormContent(IManagedForm managedForm) {
+               ScrolledForm form = managedForm.getForm();
+               form.setText(WorkbenchUiPlugin.getMessage("nodeVersionHistoryPageTitle"));
+               tk = managedForm.getToolkit();
+               Composite innerBox = form.getBody();
+               // Composite innerBox = new Composite(body, SWT.NO_FOCUS);
+               GridLayout twt = new GridLayout(1, false);
+               twt.marginWidth = twt.marginHeight = 5;
+               innerBox.setLayout(twt);
+               try {
+                       if (!currentNode.isNodeType(NodeType.MIX_VERSIONABLE)) {
+                               tk.createLabel(innerBox, WorkbenchUiPlugin.getMessage("warningUnversionableNode"));
+                       } else {
+                               createHistorySection(innerBox);
+                               createTreeSection(innerBox);
+                       }
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Unable to check if node is versionable", e);
+               }
+       }
+
+       protected void createTreeSection(Composite parent) {
+               Section section = tk.createSection(parent, Section.TWISTIE);
+               section.setLayoutData(new GridData(GridData.FILL_BOTH));
+               section.setText(WorkbenchUiPlugin.getMessage("versionTreeSectionTitle"));
+
+               Composite body = tk.createComposite(section, SWT.FILL);
+               section.setClient(body);
+               section.setExpanded(true);
+               body.setLayoutData(new GridData(GridData.FILL_BOTH));
+               body.setLayout(new GridLayout());
+
+               nodeContentProvider = new FullVersioningTreeContentProvider();
+               nodesViewer = createNodeViewer(body, nodeContentProvider);
+               nodesViewer.setInput(currentNode);
+       }
+
+       protected TreeViewer createNodeViewer(Composite parent, final ITreeContentProvider nodeContentProvider) {
+
+               final TreeViewer tmpNodeViewer = new TreeViewer(parent, SWT.MULTI);
+
+               tmpNodeViewer.getTree().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               tmpNodeViewer.setContentProvider(nodeContentProvider);
+               tmpNodeViewer.setLabelProvider(new VersionLabelProvider());
+               tmpNodeViewer.addDoubleClickListener(new JcrDClickListener(tmpNodeViewer));
+               return tmpNodeViewer;
+       }
+
+       protected void createHistorySection(Composite parent) {
+
+               // Section Layout
+               Section section = tk.createSection(parent, Section.TWISTIE);
+               section.setLayoutData(new GridData(TableWrapData.FILL_GRAB));
+               TableWrapLayout twt = new TableWrapLayout();
+               section.setLayout(twt);
+
+               // Set title of the section
+               section.setText(WorkbenchUiPlugin.getMessage("versionHistorySectionTitle"));
+
+               final Text styledText = tk.createText(section, "",
+                               SWT.FULL_SELECTION | SWT.BORDER | SWT.MULTI | SWT.WRAP | SWT.V_SCROLL);
+               section.setClient(styledText);
+               styledText.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.FILL_GRAB));
+               refreshHistory(styledText);
+               styledText.setEditable(false);
+               section.setExpanded(false);
+
+               AbstractFormPart part = new AbstractFormPart() {
+                       public void commit(boolean onSave) {
+                       }
+
+                       public void refresh() {
+                               super.refresh();
+                               refreshHistory(styledText);
+                       }
+               };
+               getManagedForm().addPart(part);
+       }
+
+       protected void refreshHistory(Text styledText) {
+               try {
+                       List<VersionDiff> lst = listHistoryDiff();
+                       StringBuffer main = new StringBuffer("");
+
+                       for (int i = lst.size() - 1; i >= 0; i--) {
+                               if (i == 0)
+                                       main.append("Creation (");
+                               else
+                                       main.append("Update " + i + " (");
+
+                               if (lst.get(i).getUserId() != null)
+                                       main.append("UserId : " + lst.get(i).getUserId());
+
+                               if (lst.get(i).getUserId() != null && lst.get(i).getUpdateTime() != null)
+                                       main.append(", ");
+
+                               if (lst.get(i).getUpdateTime() != null)
+                                       main.append("Date : " + timeFormatter.format(lst.get(i).getUpdateTime().getTime()) + ")\n");
+
+                               StringBuffer buf = new StringBuffer("");
+                               Map<String, PropertyDiff> diffs = lst.get(i).getDiffs();
+                               for (String prop : diffs.keySet()) {
+                                       PropertyDiff pd = diffs.get(prop);
+                                       // String propName = pd.getRelPath();
+                                       Value refValue = pd.getReferenceValue();
+                                       Value newValue = pd.getNewValue();
+                                       String refValueStr = "";
+                                       String newValueStr = "";
+
+                                       if (refValue != null) {
+                                               if (refValue.getType() == PropertyType.DATE) {
+                                                       refValueStr = timeFormatter.format(refValue.getDate().getTime());
+                                               } else
+                                                       refValueStr = refValue.getString();
+                                       }
+                                       if (newValue != null) {
+                                               if (newValue.getType() == PropertyType.DATE) {
+                                                       newValueStr = timeFormatter.format(newValue.getDate().getTime());
+                                               } else
+                                                       newValueStr = newValue.getString();
+                                       }
+
+                                       if (pd.getType() == PropertyDiff.MODIFIED) {
+                                               buf.append(prop).append(": ");
+                                               buf.append(refValueStr);
+                                               buf.append(" > ");
+                                               buf.append(newValueStr);
+                                               buf.append("\n");
+                                       } else if (pd.getType() == PropertyDiff.ADDED && !"".equals(newValueStr)) {
+                                               // we don't list property that have been added with an
+                                               // empty string as value
+                                               buf.append(prop).append(": ");
+                                               buf.append(" + ");
+                                               buf.append(newValueStr);
+                                               buf.append("\n");
+                                       } else if (pd.getType() == PropertyDiff.REMOVED) {
+                                               buf.append(prop).append(": ");
+                                               buf.append(" - ");
+                                               buf.append(refValueStr);
+                                               buf.append("\n");
+                                       }
+                               }
+                               buf.append("\n");
+                               main.append(buf);
+                       }
+                       styledText.setText(main.toString());
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot generate history for node", e);
+               }
+       }
+
+       public List<VersionDiff> listHistoryDiff() {
+               try {
+                       List<VersionDiff> res = new ArrayList<VersionDiff>();
+                       VersionManager versionManager = currentNode.getSession().getWorkspace().getVersionManager();
+                       VersionHistory versionHistory = versionManager.getVersionHistory(currentNode.getPath());
+
+                       VersionIterator vit = versionHistory.getAllLinearVersions();
+                       while (vit.hasNext()) {
+                               Version version = vit.nextVersion();
+                               Node node = version.getFrozenNode();
+                               Version predecessor = null;
+                               try {
+                                       predecessor = version.getLinearPredecessor();
+                               } catch (Exception e) {
+                                       // no predecessor seems to throw an exception even if it
+                                       // shouldn't...
+                               }
+                               if (predecessor == null) {// original
+                               } else {
+                                       Map<String, PropertyDiff> diffs = JcrUtils.diffProperties(predecessor.getFrozenNode(), node);
+                                       if (!diffs.isEmpty()) {
+                                               String lastUserName = null;
+                                               Calendar lastUpdate = null;
+                                               try {
+                                                       if (currentNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+                                                               lastUserName = node.getProperty(Property.JCR_LAST_MODIFIED_BY).getString();
+                                                               lastUpdate = node.getProperty(Property.JCR_LAST_MODIFIED).getDate();
+                                                       } else
+                                                               lastUpdate = version.getProperty(Property.JCR_CREATED).getDate();
+
+                                               } catch (Exception e) {
+                                                       // Silent that info is optional
+                                               }
+                                               VersionDiff vd = new VersionDiff(lastUserName, lastUpdate, diffs);
+                                               res.add(vd);
+                                       }
+                               }
+                       }
+                       return res;
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot generate history for node ");
+               }
+
+       }
+
+       @Override
+       public void setActive(boolean active) {
+               super.setActive(active);
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/StringNodeEditorInput.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/jcr/parts/StringNodeEditorInput.java
new file mode 100644 (file)
index 0000000..6aae94c
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.jcr.parts;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IPersistableElement;
+
+/**
+ * An editor input based on three strings define a node :
+ * <ul>
+ * <li>complete path to the node</li>
+ * <li>the workspace name</li>
+ * <li>the repository alias</li>
+ * </ul>
+ * In a single workspace and/or repository environment, name and alias can be
+ * null.
+ * 
+ * Note : unused for the time being.
+ */
+
+public class StringNodeEditorInput implements IEditorInput {
+       private final String path;
+       private final String repositoryAlias;
+       private final String workspaceName;
+
+       /**
+        * In order to implement a generic explorer that supports remote and multi
+        * workspaces repositories, node path can be detailed by these strings.
+        * 
+        * @param repositoryAlias
+        *            : can be null
+        * @param workspaceName
+        *            : can be null
+        * @param path
+        */
+       public StringNodeEditorInput(String repositoryAlias, String workspaceName,
+                       String path) {
+               this.path = path;
+               this.repositoryAlias = repositoryAlias;
+               this.workspaceName = workspaceName;
+       }
+
+       @SuppressWarnings("unchecked")
+       public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
+               return null;
+       }
+
+       public boolean exists() {
+               return true;
+       }
+
+       public ImageDescriptor getImageDescriptor() {
+               return null;
+       }
+
+       public String getName() {
+               return path;
+       }
+
+       public String getRepositoryAlias() {
+               return repositoryAlias;
+       }
+
+       public String getWorkspaceName() {
+               return workspaceName;
+       }
+
+       public IPersistableElement getPersistable() {
+               return null;
+       }
+
+       public String getToolTipText() {
+               return path;
+       }
+
+       public String getPath() {
+               return path;
+       }
+
+       public boolean equals(Object obj) {
+               if (this == obj)
+                       return true;
+               if (obj == null)
+                       return false;
+               if (getClass() != obj.getClass())
+                       return false;
+
+               StringNodeEditorInput other = (StringNodeEditorInput) obj;
+
+               if (!path.equals(other.getPath()))
+                       return false;
+
+               String own = other.getWorkspaceName();
+               if ((workspaceName == null && own != null)
+                               || (workspaceName != null && (own == null || !workspaceName
+                                               .equals(own))))
+                       return false;
+
+               String ora = other.getRepositoryAlias();
+               if ((repositoryAlias == null && ora != null)
+                               || (repositoryAlias != null && (ora == null || !repositoryAlias
+                                               .equals(ora))))
+                       return false;
+
+               return true;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/PartStateChanged.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/PartStateChanged.java
new file mode 100644 (file)
index 0000000..28e82c5
--- /dev/null
@@ -0,0 +1,58 @@
+package org.argeo.cms.ui.workbench.internal.useradmin;
+
+import org.argeo.cms.CmsException;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IPartListener;
+import org.eclipse.ui.IStartup;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.PlatformUI;
+
+/** Manage transaction and part refresh while updating the security model */
+public class PartStateChanged implements IPartListener, IStartup {
+       // private final static Log log = LogFactory.getLog(PartStateChanged.class);
+       // private IContextActivation contextActivation;
+
+       @Override
+       public void earlyStartup() {
+               Display.getDefault().asyncExec(new Runnable() {
+                       public void run() {
+                               try {
+                                       IWorkbenchPage iwp = PlatformUI.getWorkbench()
+                                                       .getActiveWorkbenchWindow().getActivePage();
+                                       if (iwp != null)
+                                               iwp.addPartListener(new PartStateChanged());
+                               } catch (Exception e) {
+                                       throw new CmsException(
+                                                       "Error while registering the PartStateChangedListener",
+                                                       e);
+                               }
+                       }
+               });
+       }
+
+       @Override
+       public void partActivated(IWorkbenchPart part) {
+               // Nothing to do
+       }
+
+       @Override
+       public void partBroughtToTop(IWorkbenchPart part) {
+               // Nothing to do
+       }
+
+       @Override
+       public void partClosed(IWorkbenchPart part) {
+               // Nothing to do
+       }
+
+       @Override
+       public void partDeactivated(IWorkbenchPart part) {
+               // Nothing to do
+       }
+
+       @Override
+       public void partOpened(IWorkbenchPart part) {
+               // Nothing to do
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/SecurityAdminImages.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/SecurityAdminImages.java
new file mode 100644 (file)
index 0000000..816dead
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Argeo Connect - Data management and communications
+ * Copyright (C) 2012 Argeo GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>
+ *
+ * Additional permission under GNU GPL version 3 section 7
+ *
+ * If you modify this Program, or any covered work, by linking or combining it
+ * with software covered by the terms of the Eclipse Public License, the
+ * licensors of this Program grant you additional permission to convey the
+ * resulting work. Corresponding Source for a non-source form of such a
+ * combination shall include the source code for the parts of such software
+ * which are used as well as that of the covered work.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin;
+
+import static org.argeo.cms.ui.workbench.WorkbenchUiPlugin.getImageDescriptor;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+
+/** Shared icons that must be declared programmatically . */
+public class SecurityAdminImages {
+       private final static String PREFIX = "icons/";
+
+       public final static ImageDescriptor ICON_REMOVE_DESC = getImageDescriptor(PREFIX + "delete.png");
+       public final static ImageDescriptor ICON_USER_DESC = getImageDescriptor(PREFIX + "person.png");
+
+       public final static Image ICON_USER = ICON_USER_DESC.createImage();
+       public final static Image ICON_GROUP = getImageDescriptor(PREFIX + "group.png").createImage();
+       public final static Image ICON_WORKGROUP = getImageDescriptor(PREFIX + "workgroup.png").createImage();
+       public final static Image ICON_ROLE = getImageDescriptor(PREFIX + "role.gif").createImage();
+
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/UiAdminUtils.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/UiAdminUtils.java
new file mode 100644 (file)
index 0000000..8f5588b
--- /dev/null
@@ -0,0 +1,40 @@
+package org.argeo.cms.ui.workbench.internal.useradmin;
+
+import javax.transaction.UserTransaction;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserTransactionProvider;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.services.ISourceProviderService;
+
+/** First effort to centralize back end methods used by the user admin UI */
+public class UiAdminUtils {
+       /*
+        * INTERNAL METHODS: Below methods are meant to stay here and are not part
+        * of a potential generic backend to manage the useradmin
+        */
+       /** Easily notify the ActiveWindow that the transaction had a state change */
+       public final static void notifyTransactionStateChange(
+                       UserTransaction userTransaction) {
+               try {
+                       IWorkbenchWindow aww = PlatformUI.getWorkbench()
+                                       .getActiveWorkbenchWindow();
+                       ISourceProviderService sourceProviderService = (ISourceProviderService) aww
+                                       .getService(ISourceProviderService.class);
+                       UserTransactionProvider esp = (UserTransactionProvider) sourceProviderService
+                                       .getSourceProvider(UserTransactionProvider.TRANSACTION_STATE);
+                       esp.fireTransactionStateChange();
+               } catch (Exception e) {
+                       throw new CmsException("Unable to begin transaction", e);
+               }
+       }
+
+       /**
+        * Email addresses must match this regexp pattern ({@value #EMAIL_PATTERN}.
+        * Thanks to <a href=
+        * "http://www.mkyong.com/regular-expressions/how-to-validate-email-address-with-regular-expression/"
+        * >this tip</a>.
+        */
+       public final static String EMAIL_PATTERN = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/UiUserAdminListener.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/UiUserAdminListener.java
new file mode 100644 (file)
index 0000000..e51d690
--- /dev/null
@@ -0,0 +1,27 @@
+package org.argeo.cms.ui.workbench.internal.useradmin;
+
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** Convenience class to insure the call to refresh is done in the UI thread */
+public abstract class UiUserAdminListener implements UserAdminListener {
+
+       private final Display display;
+
+       public UiUserAdminListener(Display display) {
+               this.display = display;
+       }
+
+       @Override
+       public void roleChanged(final UserAdminEvent event) {
+               display.asyncExec(new Runnable() {
+                       @Override
+                       public void run() {
+                               roleChangedToUiThread(event);
+                       }
+               });
+       }
+
+       public abstract void roleChangedToUiThread(UserAdminEvent event);
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/UserAdminWrapper.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/UserAdminWrapper.java
new file mode 100644 (file)
index 0000000..e4efcc7
--- /dev/null
@@ -0,0 +1,127 @@
+package org.argeo.cms.ui.workbench.internal.useradmin;
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.transaction.Status;
+import javax.transaction.UserTransaction;
+
+import org.argeo.cms.CmsException;
+import org.argeo.node.NodeConstants;
+import org.argeo.osgi.useradmin.UserAdminConf;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** Centralise interaction with the UserAdmin in this bundle */
+public class UserAdminWrapper {
+
+       private UserAdmin userAdmin;
+       private ServiceReference<UserAdmin> userAdminServiceReference;
+       private UserTransaction userTransaction;
+
+       // First effort to simplify UX while managing users and groups
+       public final static boolean COMMIT_ON_SAVE = true;
+
+       // Registered listeners
+       List<UserAdminListener> listeners = new ArrayList<UserAdminListener>();
+
+       /**
+        * Starts a transaction if necessary. Should always been called together
+        * with {@link UserAdminWrapper#commitOrNotifyTransactionStateChange()} once
+        * the security model changes have been performed.
+        */
+       public UserTransaction beginTransactionIfNeeded() {
+               try {
+                       // UserTransaction userTransaction = getUserTransaction();
+                       if (userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION) {
+                               userTransaction.begin();
+                               // UiAdminUtils.notifyTransactionStateChange(userTransaction);
+                       }
+                       return userTransaction;
+               } catch (Exception e) {
+                       throw new CmsException("Unable to begin transaction", e);
+               }
+       }
+
+       /**
+        * Depending on the current application configuration, it will either commit
+        * the current transaction or throw a notification that the transaction
+        * state has changed (In the later case, it must be called from the UI
+        * thread).
+        */
+       public void commitOrNotifyTransactionStateChange() {
+               try {
+                       // UserTransaction userTransaction = getUserTransaction();
+                       if (userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION)
+                               return;
+
+                       if (UserAdminWrapper.COMMIT_ON_SAVE)
+                               userTransaction.commit();
+                       else
+                               UiAdminUtils.notifyTransactionStateChange(userTransaction);
+               } catch (Exception e) {
+                       throw new CmsException("Unable to clean transaction", e);
+               }
+       }
+
+       // TODO implement safer mechanism
+       public void addListener(UserAdminListener userAdminListener) {
+               if (!listeners.contains(userAdminListener))
+                       listeners.add(userAdminListener);
+       }
+
+       public void removeListener(UserAdminListener userAdminListener) {
+               if (listeners.contains(userAdminListener))
+                       listeners.remove(userAdminListener);
+       }
+
+       public void notifyListeners(UserAdminEvent event) {
+               for (UserAdminListener listener : listeners)
+                       listener.roleChanged(event);
+       }
+
+       public Map<String, String> getKnownBaseDns(boolean onlyWritable) {
+               Map<String, String> dns = new HashMap<String, String>();
+               for (String uri : userAdminServiceReference.getPropertyKeys()) {
+                       if (!uri.startsWith("/"))
+                               continue;
+                       Dictionary<String, ?> props = UserAdminConf.uriAsProperties(uri);
+                       String readOnly = UserAdminConf.readOnly.getValue(props);
+                       String baseDn = UserAdminConf.baseDn.getValue(props);
+
+                       if (onlyWritable && "true".equals(readOnly))
+                               continue;
+                       if (baseDn.equalsIgnoreCase(NodeConstants.ROLES_BASEDN))
+                               continue;
+                       dns.put(baseDn, uri);
+               }
+               return dns;
+       }
+
+       public UserAdmin getUserAdmin() {
+               return userAdmin;
+       }
+
+       public UserTransaction getUserTransaction() {
+               return userTransaction;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdmin(UserAdmin userAdmin) {
+               this.userAdmin = userAdmin;
+       }
+
+       public void setUserTransaction(UserTransaction userTransaction) {
+               this.userTransaction = userTransaction;
+       }
+
+       public void setUserAdminServiceReference(
+                       ServiceReference<UserAdmin> userAdminServiceReference) {
+               this.userAdminServiceReference = userAdminServiceReference;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/DeleteGroups.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/DeleteGroups.java
new file mode 100644 (file)
index 0000000..3e8d12f
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.commands;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UserEditorInput;
+import org.argeo.cms.util.UserAdminUtils;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Delete the selected groups */
+public class DeleteGroups extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".deleteGroups";
+
+       /* DEPENDENCY INJECTION */
+       private UserAdminWrapper userAdminWrapper;
+
+       @SuppressWarnings("unchecked")
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               ISelection selection = HandlerUtil.getCurrentSelection(event);
+               if (selection.isEmpty())
+                       return null;
+
+               List<Group> groups = new ArrayList<Group>();
+               Iterator<Group> it = ((IStructuredSelection) selection).iterator();
+               StringBuilder builder = new StringBuilder();
+               while (it.hasNext()) {
+                       Group currGroup = it.next();
+                       String groupName = UserAdminUtils.getUserLocalId(currGroup.getName());
+                       // TODO add checks
+                       builder.append(groupName).append("; ");
+                       groups.add(currGroup);
+               }
+
+               if (!MessageDialog.openQuestion(HandlerUtil.getActiveShell(event), "Delete Groups", "Are you sure that you "
+                               + "want to delete these groups?\n" + builder.substring(0, builder.length() - 2)))
+                       return null;
+
+               userAdminWrapper.beginTransactionIfNeeded();
+               UserAdmin userAdmin = userAdminWrapper.getUserAdmin();
+               IWorkbenchPage iwp = HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
+               for (Group group : groups) {
+                       String groupName = group.getName();
+                       // TODO find a way to close the editor cleanly if opened. Cannot be
+                       // done through the UserAdminListeners, it causes a
+                       // java.util.ConcurrentModificationException because disposing the
+                       // editor unregisters and disposes the listener
+                       IEditorPart part = iwp.findEditor(new UserEditorInput(groupName));
+                       if (part != null)
+                               iwp.closeEditor(part, false);
+                       userAdmin.removeRole(groupName);
+               }
+               userAdminWrapper.commitOrNotifyTransactionStateChange();
+
+               // Update the view
+               for (Group group : groups) {
+                       userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, group));
+               }
+
+               return null;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/DeleteUsers.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/DeleteUsers.java
new file mode 100644 (file)
index 0000000..ee36648
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.commands;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UserEditorInput;
+import org.argeo.cms.util.UserAdminUtils;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Delete the selected users */
+public class DeleteUsers extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".deleteUsers";
+
+       /* DEPENDENCY INJECTION */
+       private UserAdminWrapper userAdminWrapper;
+
+       @SuppressWarnings("unchecked")
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               ISelection selection = HandlerUtil.getCurrentSelection(event);
+               if (selection.isEmpty())
+                       return null;
+
+               Iterator<User> it = ((IStructuredSelection) selection).iterator();
+               List<User> users = new ArrayList<User>();
+               StringBuilder builder = new StringBuilder();
+
+               while (it.hasNext()) {
+                       User currUser = it.next();
+                       String userName = UserAdminUtils.getUserLocalId(currUser.getName());
+                       if (UserAdminUtils.isCurrentUser(currUser)) {
+                               MessageDialog.openError(HandlerUtil.getActiveShell(event), "Deletion forbidden",
+                                               "You cannot delete your own user this way.");
+                               return null;
+                       }
+                       builder.append(userName).append("; ");
+                       users.add(currUser);
+               }
+
+               if (!MessageDialog.openQuestion(HandlerUtil.getActiveShell(event), "Delete Users",
+                               "Are you sure that you want to delete these users?\n" + builder.substring(0, builder.length() - 2)))
+                       return null;
+
+               userAdminWrapper.beginTransactionIfNeeded();
+               UserAdmin userAdmin = userAdminWrapper.getUserAdmin();
+               IWorkbenchPage iwp = HandlerUtil.getActiveWorkbenchWindow(event).getActivePage();
+
+               for (User user : users) {
+                       String userName = user.getName();
+                       // TODO find a way to close the editor cleanly if opened. Cannot be
+                       // done through the UserAdminListeners, it causes a
+                       // java.util.ConcurrentModificationException because disposing the
+                       // editor unregisters and disposes the listener
+                       IEditorPart part = iwp.findEditor(new UserEditorInput(userName));
+                       if (part != null)
+                               iwp.closeEditor(part, false);
+                       userAdmin.removeRole(userName);
+               }
+               userAdminWrapper.commitOrNotifyTransactionStateChange();
+
+               for (User user : users) {
+                       userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, user));
+               }
+               return null;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/ForceRefresh.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/ForceRefresh.java
new file mode 100644 (file)
index 0000000..86a2eed
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.commands;
+
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.GroupsView;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UsersView;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Retrieve the active view or editor and call forceRefresh method if defined */
+public class ForceRefresh extends AbstractHandler {
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               IWorkbenchWindow iww = HandlerUtil.getActiveWorkbenchWindow(event);
+               if (iww == null)
+                       return null;
+               IWorkbenchPage activePage = iww.getActivePage();
+               IWorkbenchPart part = activePage.getActivePart();
+               if (part instanceof UsersView)
+                       ((UsersView) part).refresh();
+               else if (part instanceof GroupsView)
+                       ((GroupsView) part).refresh();
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/NewGroup.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/NewGroup.java
new file mode 100644 (file)
index 0000000..51a14fc
--- /dev/null
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.commands;
+
+import java.util.Dictionary;
+import java.util.Map;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.osgi.useradmin.UserAdminConf;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Create a new group */
+public class NewGroup extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".newGroup";
+
+       /* DEPENDENCY INJECTION */
+       private UserAdminWrapper userAdminWrapper;
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               NewGroupWizard newGroupWizard = new NewGroupWizard();
+               newGroupWizard.setWindowTitle("Group creation");
+               WizardDialog dialog = new WizardDialog(
+                               HandlerUtil.getActiveShell(event), newGroupWizard);
+               dialog.open();
+               return null;
+       }
+
+       private class NewGroupWizard extends Wizard {
+
+               // Pages
+               private MainGroupInfoWizardPage mainGroupInfo;
+
+               // UI fields
+               private Text dNameTxt, commonNameTxt, descriptionTxt;
+               private Combo baseDnCmb;
+
+               public NewGroupWizard() {
+               }
+
+               @Override
+               public void addPages() {
+                       mainGroupInfo = new MainGroupInfoWizardPage();
+                       addPage(mainGroupInfo);
+               }
+
+               @SuppressWarnings({ "rawtypes", "unchecked" })
+               @Override
+               public boolean performFinish() {
+                       if (!canFinish())
+                               return false;
+                       String commonName = commonNameTxt.getText();
+                       try {
+                               userAdminWrapper.beginTransactionIfNeeded();
+                               String dn = getDn(commonName);
+                               Group group = (Group) userAdminWrapper.getUserAdmin()
+                                               .createRole(dn, Role.GROUP);
+                               Dictionary props = group.getProperties();
+                               String descStr = descriptionTxt.getText();
+                               if (EclipseUiUtils.notEmpty(descStr))
+                                       props.put(LdapAttrs.description.name(), descStr);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null,
+                                               UserAdminEvent.ROLE_CREATED, group));
+                               return true;
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot create new group " + commonName, e);
+                               return false;
+                       }
+               }
+
+               private class MainGroupInfoWizardPage extends WizardPage implements
+                               FocusListener, ArgeoNames {
+                       private static final long serialVersionUID = -3150193365151601807L;
+
+                       public MainGroupInfoWizardPage() {
+                               super("Main");
+                               setTitle("General information");
+                               setMessage("Please choose a domain, provide a common name "
+                                               + "and a free description");
+                       }
+
+                       @Override
+                       public void createControl(Composite parent) {
+                               Composite bodyCmp = new Composite(parent, SWT.NONE);
+                               setControl(bodyCmp);
+                               bodyCmp.setLayout(new GridLayout(2, false));
+
+                               dNameTxt = EclipseUiUtils.createGridLT(bodyCmp,
+                                               "Distinguished name");
+                               dNameTxt.setEnabled(false);
+
+                               baseDnCmb = createGridLC(bodyCmp, "Base DN");
+                               // Initialise before adding the listener to avoid NPE
+                               initialiseDnCmb(baseDnCmb);
+                               baseDnCmb.addFocusListener(this);
+
+                               commonNameTxt = EclipseUiUtils.createGridLT(bodyCmp,
+                                               "Common name");
+                               commonNameTxt.addFocusListener(this);
+
+                               Label descLbl = new Label(bodyCmp, SWT.LEAD);
+                               descLbl.setText("Description");
+                               descLbl.setLayoutData(new GridData(SWT.RIGHT, SWT.TOP, false,
+                                               false));
+                               descriptionTxt = new Text(bodyCmp, SWT.LEAD | SWT.MULTI
+                                               | SWT.WRAP | SWT.BORDER);
+                               descriptionTxt.setLayoutData(EclipseUiUtils.fillAll());
+                               descriptionTxt.addFocusListener(this);
+
+                               // Initialize buttons
+                               setPageComplete(false);
+                               getContainer().updateButtons();
+                       }
+
+                       @Override
+                       public void focusLost(FocusEvent event) {
+                               String name = commonNameTxt.getText();
+                               if (EclipseUiUtils.isEmpty(name))
+                                       dNameTxt.setText("");
+                               else
+                                       dNameTxt.setText(getDn(name));
+
+                               String message = checkComplete();
+                               if (message != null) {
+                                       setMessage(message, WizardPage.ERROR);
+                                       setPageComplete(false);
+                               } else {
+                                       setMessage("Complete", WizardPage.INFORMATION);
+                                       setPageComplete(true);
+                               }
+                               getContainer().updateButtons();
+                       }
+
+                       @Override
+                       public void focusGained(FocusEvent event) {
+                       }
+
+                       /** @return the error message or null if complete */
+                       protected String checkComplete() {
+                               String name = commonNameTxt.getText();
+
+                               if (name.trim().equals(""))
+                                       return "Common name must not be empty";
+                               Role role = userAdminWrapper.getUserAdmin()
+                                               .getRole(getDn(name));
+                               if (role != null)
+                                       return "Group " + name + " already exists";
+                               return null;
+                       }
+
+                       @Override
+                       public void setVisible(boolean visible) {
+                               super.setVisible(visible);
+                               if (visible)
+                                       if (baseDnCmb.getSelectionIndex() == -1)
+                                               baseDnCmb.setFocus();
+                                       else
+                                               commonNameTxt.setFocus();
+                       }
+               }
+
+               private Map<String, String> getDns() {
+                       return userAdminWrapper.getKnownBaseDns(true);
+               }
+
+               private String getDn(String cn) {
+                       Map<String, String> dns = getDns();
+                       String bdn = baseDnCmb.getText();
+                       if (EclipseUiUtils.notEmpty(bdn)) {
+                               Dictionary<String, ?> props = UserAdminConf.uriAsProperties(dns
+                                               .get(bdn));
+                               String dn = LdapAttrs.cn.name() + "=" + cn + ","
+                                               + UserAdminConf.groupBase.getValue(props) + "," + bdn;
+                               return dn;
+                       }
+                       return null;
+               }
+
+               private void initialiseDnCmb(Combo combo) {
+                       Map<String, String> dns = userAdminWrapper.getKnownBaseDns(true);
+                       if (dns.isEmpty())
+                               throw new CmsException(
+                                               "No writable base dn found. Cannot create group");
+                       combo.setItems(dns.keySet().toArray(new String[0]));
+                       if (dns.size() == 1)
+                               combo.select(0);
+               }
+       }
+
+       private Combo createGridLC(Composite parent, String label) {
+               Label lbl = new Label(parent, SWT.LEAD);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               Combo combo = new Combo(parent, SWT.LEAD | SWT.BORDER | SWT.READ_ONLY);
+               combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               return combo;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/NewUser.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/NewUser.java
new file mode 100644 (file)
index 0000000..94aa635
--- /dev/null
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.commands;
+
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Map;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.useradmin.UiAdminUtils;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.osgi.useradmin.UserAdminConf;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.handlers.HandlerUtil;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Open a wizard that enables creation of a new user. */
+public class NewUser extends AbstractHandler {
+       // private final static Log log = LogFactory.getLog(NewUser.class);
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".newUser";
+
+       /* DEPENDENCY INJECTION */
+       private UserAdminWrapper userAdminWrapper;
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               NewUserWizard newUserWizard = new NewUserWizard();
+               newUserWizard.setWindowTitle("User creation");
+               WizardDialog dialog = new WizardDialog(
+                               HandlerUtil.getActiveShell(event), newUserWizard);
+               dialog.open();
+               return null;
+       }
+
+       private class NewUserWizard extends Wizard {
+
+               // pages
+               private MainUserInfoWizardPage mainUserInfo;
+
+               // End user fields
+               private Text dNameTxt, usernameTxt, firstNameTxt, lastNameTxt,
+                               primaryMailTxt, pwd1Txt, pwd2Txt;
+               private Combo baseDnCmb;
+
+               public NewUserWizard() {
+
+               }
+
+               @Override
+               public void addPages() {
+                       mainUserInfo = new MainUserInfoWizardPage();
+                       addPage(mainUserInfo);
+                       String message = "Default wizard that also eases user creation tests:\n "
+                                       + "Mail and last name are automatically "
+                                       + "generated form the uid. Password are defauted to 'demo'.";
+                       mainUserInfo.setMessage(message, WizardPage.WARNING);
+               }
+
+               @SuppressWarnings({ "rawtypes", "unchecked" })
+               @Override
+               public boolean performFinish() {
+                       if (!canFinish())
+                               return false;
+                       String username = mainUserInfo.getUsername();
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       try {
+                               User user = (User) userAdminWrapper.getUserAdmin().createRole(
+                                               getDn(username), Role.USER);
+
+                               Dictionary props = user.getProperties();
+
+                               String lastNameStr = lastNameTxt.getText();
+                               if (EclipseUiUtils.notEmpty(lastNameStr))
+                                       props.put(LdapAttrs.sn.name(), lastNameStr);
+
+                               String firstNameStr = firstNameTxt.getText();
+                               if (EclipseUiUtils.notEmpty(firstNameStr))
+                                       props.put(LdapAttrs.givenName.name(), firstNameStr);
+
+                               String cn = UserAdminUtils.buildDefaultCn(firstNameStr,
+                                               lastNameStr);
+                               if (EclipseUiUtils.notEmpty(cn))
+                                       props.put(LdapAttrs.cn.name(), cn);
+
+                               String mailStr = primaryMailTxt.getText();
+                               if (EclipseUiUtils.notEmpty(mailStr))
+                                       props.put(LdapAttrs.mail.name(), mailStr);
+
+                               char[] password = mainUserInfo.getPassword();
+                               user.getCredentials().put(null, password);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null,
+                                               UserAdminEvent.ROLE_CREATED, user));
+                               return true;
+                       } catch (Exception e) {
+                               ErrorFeedback.show("Cannot create new user " + username, e);
+                               return false;
+                       }
+               }
+
+               private class MainUserInfoWizardPage extends WizardPage implements
+                               ModifyListener, ArgeoNames {
+                       private static final long serialVersionUID = -3150193365151601807L;
+
+                       public MainUserInfoWizardPage() {
+                               super("Main");
+                               setTitle("Required Information");
+                       }
+
+                       @Override
+                       public void createControl(Composite parent) {
+                               Composite composite = new Composite(parent, SWT.NONE);
+                               composite.setLayout(new GridLayout(2, false));
+                               dNameTxt = EclipseUiUtils.createGridLT(composite,
+                                               "Distinguished name", this);
+                               dNameTxt.setEnabled(false);
+
+                               baseDnCmb = createGridLC(composite, "Base DN");
+                               initialiseDnCmb(baseDnCmb);
+                               baseDnCmb.addModifyListener(this);
+                               baseDnCmb.addModifyListener(new ModifyListener() {
+                                       private static final long serialVersionUID = -1435351236582736843L;
+
+                                       @Override
+                                       public void modifyText(ModifyEvent event) {
+                                               String name = usernameTxt.getText();
+                                               dNameTxt.setText(getDn(name));
+                                       }
+                               });
+
+                               usernameTxt = EclipseUiUtils.createGridLT(composite,
+                                               "Local ID", this);
+                               usernameTxt.addModifyListener(new ModifyListener() {
+                                       private static final long serialVersionUID = -1435351236582736843L;
+
+                                       @Override
+                                       public void modifyText(ModifyEvent event) {
+                                               String name = usernameTxt.getText();
+                                               if (name.trim().equals("")) {
+                                                       dNameTxt.setText("");
+                                                       lastNameTxt.setText("");
+                                                       primaryMailTxt.setText("");
+                                                       pwd1Txt.setText("");
+                                                       pwd2Txt.setText("");
+                                               } else {
+                                                       dNameTxt.setText(getDn(name));
+                                                       lastNameTxt.setText(name.toUpperCase());
+                                                       primaryMailTxt.setText(getMail(name));
+                                                       pwd1Txt.setText("demo");
+                                                       pwd2Txt.setText("demo");
+                                               }
+                                       }
+                               });
+
+                               primaryMailTxt = EclipseUiUtils.createGridLT(composite,
+                                               "Email", this);
+                               firstNameTxt = EclipseUiUtils.createGridLT(composite,
+                                               "First name", this);
+                               lastNameTxt = EclipseUiUtils.createGridLT(composite,
+                                               "Last name", this);
+                               pwd1Txt = EclipseUiUtils.createGridLP(composite, "Password",
+                                               this);
+                               pwd2Txt = EclipseUiUtils.createGridLP(composite,
+                                               "Repeat password", this);
+                               setControl(composite);
+
+                               // Initialize buttons
+                               setPageComplete(false);
+                               getContainer().updateButtons();
+                       }
+
+                       @Override
+                       public void modifyText(ModifyEvent event) {
+                               String message = checkComplete();
+                               if (message != null) {
+                                       setMessage(message, WizardPage.ERROR);
+                                       setPageComplete(false);
+                               } else {
+                                       setMessage("Complete", WizardPage.INFORMATION);
+                                       setPageComplete(true);
+                               }
+                               getContainer().updateButtons();
+                       }
+
+                       /** @return error message or null if complete */
+                       protected String checkComplete() {
+                               String name = usernameTxt.getText();
+
+                               if (name.trim().equals(""))
+                                       return "User name must not be empty";
+                               Role role = userAdminWrapper.getUserAdmin()
+                                               .getRole(getDn(name));
+                               if (role != null)
+                                       return "User " + name + " already exists";
+                               if (!primaryMailTxt.getText().matches(UiAdminUtils.EMAIL_PATTERN))
+                                       return "Not a valid email address";
+                               if (lastNameTxt.getText().trim().equals(""))
+                                       return "Specify a last name";
+                               if (pwd1Txt.getText().trim().equals(""))
+                                       return "Specify a password";
+                               if (pwd2Txt.getText().trim().equals(""))
+                                       return "Repeat the password";
+                               if (!pwd2Txt.getText().equals(pwd1Txt.getText()))
+                                       return "Passwords are different";
+                               return null;
+                       }
+
+                       @Override
+                       public void setVisible(boolean visible) {
+                               super.setVisible(visible);
+                               if (visible)
+                                       if (baseDnCmb.getSelectionIndex() == -1)
+                                               baseDnCmb.setFocus();
+                                       else
+                                               usernameTxt.setFocus();
+                       }
+
+                       public String getUsername() {
+                               return usernameTxt.getText();
+                       }
+
+                       public char[] getPassword() {
+                               return pwd1Txt.getTextChars();
+                       }
+
+               }
+
+               private Map<String, String> getDns() {
+                       return userAdminWrapper.getKnownBaseDns(true);
+               }
+
+               private String getDn(String uid) {
+                       Map<String, String> dns = getDns();
+                       String bdn = baseDnCmb.getText();
+                       if (EclipseUiUtils.notEmpty(bdn)) {
+                               Dictionary<String, ?> props = UserAdminConf.uriAsProperties(dns
+                                               .get(bdn));
+                               String dn = LdapAttrs.uid.name() + "=" + uid + ","
+                                               + UserAdminConf.userBase.getValue(props) + "," + bdn;
+                               return dn;
+                       }
+                       return null;
+               }
+
+               private void initialiseDnCmb(Combo combo) {
+                       Map<String, String> dns = userAdminWrapper.getKnownBaseDns(true);
+                       if (dns.isEmpty())
+                               throw new CmsException(
+                                               "No writable base dn found. Cannot create user");
+                       combo.setItems(dns.keySet().toArray(new String[0]));
+                       if (dns.size() == 1)
+                               combo.select(0);
+               }
+
+               private String getMail(String username) {
+                       if (baseDnCmb.getSelectionIndex() == -1)
+                               return null;
+                       String baseDn = baseDnCmb.getText();
+                       try {
+                               LdapName name = new LdapName(baseDn);
+                               List<Rdn> rdns = name.getRdns();
+                               return username + "@" + (String) rdns.get(1).getValue() + '.'
+                                               + (String) rdns.get(0).getValue();
+                       } catch (InvalidNameException e) {
+                               throw new CmsException("Unable to generate mail for "
+                                               + username + " with base dn " + baseDn, e);
+                       }
+               }
+       }
+
+       private Combo createGridLC(Composite parent, String label) {
+               Label lbl = new Label(parent, SWT.LEAD);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               Combo combo = new Combo(parent, SWT.LEAD | SWT.BORDER | SWT.READ_ONLY);
+               combo.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               return combo;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/SaveArgeoUser.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/SaveArgeoUser.java
new file mode 100644 (file)
index 0000000..ef1ddbd
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.commands;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Save the currently edited Argeo user. */
+public class SaveArgeoUser extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".saveArgeoUser";
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               try {
+                       IWorkbenchPart iwp = HandlerUtil.getActiveWorkbenchWindow(event)
+                                       .getActivePage().getActivePart();
+                       if (!(iwp instanceof IEditorPart))
+                               return null;
+                       IEditorPart editor = (IEditorPart) iwp;
+                       editor.doSave(null);
+               } catch (Exception e) {
+                       MessageDialog.openError(Display.getDefault().getActiveShell(),
+                                       "Error", "Cannot save user: " + e.getMessage());
+               }
+               return null;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/UserBatchUpdate.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/UserBatchUpdate.java
new file mode 100644 (file)
index 0000000..7d29e8f
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.commands;
+
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UserBatchUpdateWizard;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.ui.handlers.HandlerUtil;
+
+/** Launch a wizard to perform batch process on users */
+public class UserBatchUpdate extends AbstractHandler {
+
+       /* DEPENDENCY INJECTION */
+       private UserAdminWrapper uaWrapper;
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               UserBatchUpdateWizard wizard = new UserBatchUpdateWizard(uaWrapper);
+               wizard.setWindowTitle("User batch processing");
+               WizardDialog dialog = new WizardDialog(
+                               HandlerUtil.getActiveShell(event), wizard);
+               dialog.open();
+               return null;
+       }
+
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.uaWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/UserTransactionHandler.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/commands/UserTransactionHandler.java
new file mode 100644 (file)
index 0000000..69fd071
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.commands;
+
+import javax.transaction.Status;
+import javax.transaction.UserTransaction;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.useradmin.UiAdminUtils;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Manage the transaction that is bound to the current perspective */
+public class UserTransactionHandler extends AbstractHandler {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".userTransactionHandler";
+
+       public final static String PARAM_COMMAND_ID = "param.commandId";
+
+       public final static String TRANSACTION_BEGIN = "transaction.begin";
+       public final static String TRANSACTION_COMMIT = "transaction.commit";
+       public final static String TRANSACTION_ROLLBACK = "transaction.rollback";
+
+       /* DEPENDENCY INJECTION */
+       private UserAdminWrapper userAdminWrapper;
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               String commandId = event.getParameter(PARAM_COMMAND_ID);
+               final UserTransaction userTransaction = userAdminWrapper
+                               .getUserTransaction();
+               try {
+                       if (TRANSACTION_BEGIN.equals(commandId)) {
+                               if (userTransaction.getStatus() != Status.STATUS_NO_TRANSACTION)
+                                       throw new CmsException("A transaction already exists");
+                               else
+                                       userTransaction.begin();
+                       } else if (TRANSACTION_COMMIT.equals(commandId)) {
+                               if (userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION)
+                                       throw new CmsException("No transaction.");
+                               else
+                                       userTransaction.commit();
+                       } else if (TRANSACTION_ROLLBACK.equals(commandId)) {
+                               if (userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION)
+                                       throw new CmsException("No transaction to rollback.");
+                               else {
+                                       userTransaction.rollback();
+                                       userAdminWrapper.notifyListeners(new UserAdminEvent(null,
+                                                       UserAdminEvent.ROLE_CHANGED, null));
+                               }
+                       }
+
+                       UiAdminUtils.notifyTransactionStateChange(userTransaction);
+                       // Try to remove invalid thread access errors when managing users.
+                       // HandlerUtil.getActivePart(event).getSite().getShell().getDisplay()
+                       // .asyncExec(new Runnable() {
+                       // @Override
+                       // public void run() {
+                       // UiAdminUtils
+                       // .notifyTransactionStateChange(userTransaction);
+                       // }
+                       // });
+
+               } catch (CmsException e) {
+                       throw e;
+               } catch (Exception e) {
+                       throw new CmsException("Unable to call " + commandId + " on "
+                                       + userTransaction, e);
+               }
+               return null;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/ArgeoUserEditorInput.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/ArgeoUserEditorInput.java
new file mode 100644 (file)
index 0000000..71089f6
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.parts;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IPersistableElement;
+
+/** Editor input for an Argeo user. */
+public class ArgeoUserEditorInput implements IEditorInput {
+       private final String username;
+
+       public ArgeoUserEditorInput(String username) {
+               this.username = username;
+       }
+
+       @SuppressWarnings("unchecked")
+       public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
+               return null;
+       }
+
+       public boolean exists() {
+               return username != null;
+       }
+
+       public ImageDescriptor getImageDescriptor() {
+               return null;
+       }
+
+       public String getName() {
+               return username != null ? username : "<new user>";
+       }
+
+       public IPersistableElement getPersistable() {
+               return null;
+       }
+
+       public String getToolTipText() {
+               return username != null ? username : "<new user>";
+       }
+
+       public boolean equals(Object obj) {
+               if (!(obj instanceof ArgeoUserEditorInput))
+                       return false;
+               if (((ArgeoUserEditorInput) obj).getUsername() == null)
+                       return false;
+               return ((ArgeoUserEditorInput) obj).getUsername().equals(username);
+       }
+
+       public String getUsername() {
+               return username;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/GroupMainPage.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/GroupMainPage.java
new file mode 100644 (file)
index 0000000..6987a21
--- /dev/null
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.parts;
+
+import static org.argeo.cms.util.UserAdminUtils.setProperty;
+import static org.argeo.naming.LdapAttrs.businessCategory;
+import static org.argeo.naming.LdapAttrs.description;
+import static org.argeo.node.NodeInstance.WORKGROUP;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.transaction.UserTransaction;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.workbench.CmsWorkbenchStyles;
+import org.argeo.cms.ui.workbench.internal.useradmin.SecurityAdminImages;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UserEditor.GroupChangeListener;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UserEditor.MainInfoListener;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.CommonNameLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.MailLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.RoleIconLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserFilter;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserTableDefaultDClickListener;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeInstance;
+import org.argeo.node.NodeUtils;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Cursor;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.ui.forms.AbstractFormPart;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.SectionPart;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+import org.eclipse.ui.forms.widgets.Section;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Display/edit main properties of a given group */
+public class GroupMainPage extends FormPage implements ArgeoNames {
+       final static String ID = "GroupEditor.mainPage";
+
+       private final UserEditor editor;
+       private final NodeInstance nodeInstance;
+       private final UserAdminWrapper userAdminWrapper;
+       private final Session session;
+
+       public GroupMainPage(FormEditor editor, UserAdminWrapper userAdminWrapper, Repository repository,
+                       NodeInstance nodeInstance) {
+               super(editor, ID, "Main");
+               try {
+                       session = repository.login();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot retrieve session of in MainGroupPage constructor", e);
+               }
+               this.editor = (UserEditor) editor;
+               this.userAdminWrapper = userAdminWrapper;
+               this.nodeInstance = nodeInstance;
+       }
+
+       protected void createFormContent(final IManagedForm mf) {
+               ScrolledForm form = mf.getForm();
+               Composite body = form.getBody();
+               GridLayout mainLayout = new GridLayout();
+               body.setLayout(mainLayout);
+               Group group = (Group) editor.getDisplayedUser();
+               appendOverviewPart(body, group);
+               appendMembersPart(body, group);
+       }
+
+       @Override
+       public void dispose() {
+               JcrUtils.logoutQuietly(session);
+               super.dispose();
+       }
+
+       /** Creates the general section */
+       protected void appendOverviewPart(final Composite parent, final Group group) {
+               FormToolkit tk = getManagedForm().getToolkit();
+               Composite body = addSection(tk, parent);
+               // GridLayout layout = new GridLayout(5, false);
+               GridLayout layout = new GridLayout(2, false);
+               body.setLayout(layout);
+
+               String cn = UserAdminUtils.getProperty(group, LdapAttrs.cn.name());
+               createReadOnlyLT(body, "Name", cn);
+               // Text dnTxt = createReadOnlyLT(body, "DN", group.getName());
+               createReadOnlyLT(body, "Domain", UserAdminUtils.getDomainName(group));
+
+               // Description
+               Label descLbl = new Label(body, SWT.LEAD);
+               descLbl.setFont(EclipseUiUtils.getBoldFont(body));
+               descLbl.setText("Description");
+               descLbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false, 2, 1));
+               final Text descTxt = new Text(body, SWT.LEAD | SWT.MULTI | SWT.WRAP | SWT.BORDER);
+               GridData gd = EclipseUiUtils.fillAll();
+               gd.heightHint = 50;
+               gd.horizontalSpan = 2;
+               descTxt.setLayoutData(gd);
+
+               // Mark as workgroup
+               Link markAsWorkgroupLk = new Link(body, SWT.NONE);
+               markAsWorkgroupLk.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false, 2, 1));
+
+               // create form part (controller)
+               final AbstractFormPart part = new SectionPart((Section) body.getParent()) {
+
+                       private MainInfoListener listener;
+
+                       @Override
+                       public void initialize(IManagedForm form) {
+                               super.initialize(form);
+                               listener = editor.new MainInfoListener(parent.getDisplay(), this);
+                               userAdminWrapper.addListener(listener);
+                       }
+
+                       @Override
+                       public void dispose() {
+                               userAdminWrapper.removeListener(listener);
+                               super.dispose();
+                       }
+
+                       public void commit(boolean onSave) {
+                               // group.getProperties().put(LdapAttrs.description.name(), descTxt.getText());
+                               setProperty(group, description, descTxt.getText());
+                               super.commit(onSave);
+                       }
+
+                       @Override
+                       public void refresh() {
+                               // dnTxt.setText(group.getName());
+                               // cnTxt.setText(UserAdminUtils.getProperty(group, LdapAttrs.cn.name()));
+                               descTxt.setText(UserAdminUtils.getProperty(group, LdapAttrs.description.name()));
+                               Node workgroupHome = NodeUtils.getGroupHome(session, cn);
+                               if (workgroupHome == null)
+                                       markAsWorkgroupLk.setText("<a>Mark as workgroup</a>");
+                               else
+                                       markAsWorkgroupLk.setText("Configured as workgroup");
+                               parent.layout(true, true);
+                               super.refresh();
+                       }
+               };
+
+               markAsWorkgroupLk.addSelectionListener(new SelectionAdapter() {
+                       private static final long serialVersionUID = -6439340898096365078L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+
+                               boolean confirmed = MessageDialog.openConfirm(parent.getShell(), "Mark as workgroup",
+                                               "Are you sure you want to mark " + cn + " as being a workgroup? ");
+                               if (confirmed) {
+                                       Node workgroupHome = NodeUtils.getGroupHome(session, cn);
+                                       if (workgroupHome != null)
+                                               return; // already marked as workgroup, do nothing
+                                       else
+                                               try {
+                                                       // improve transaction management
+                                                       userAdminWrapper.beginTransactionIfNeeded();
+                                                       nodeInstance.createWorkgroup(new LdapName(group.getName()));
+                                                       setProperty(group, businessCategory, WORKGROUP);
+                                                       userAdminWrapper.commitOrNotifyTransactionStateChange();
+                                                       userAdminWrapper
+                                                                       .notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+                                                       part.refresh();
+                                               } catch (InvalidNameException e1) {
+                                                       throw new CmsException("Cannot create Workgroup for " + group.toString(), e1);
+                                               }
+
+                               }
+                       }
+               });
+
+               ModifyListener defaultListener = editor.new FormPartML(part);
+               descTxt.addModifyListener(defaultListener);
+               getManagedForm().addPart(part);
+       }
+
+       /** Filtered table with members. Has drag and drop ability */
+       protected void appendMembersPart(Composite parent, Group group) {
+               FormToolkit tk = getManagedForm().getToolkit();
+               Section section = tk.createSection(parent, Section.TITLE_BAR);
+               section.setText("Members");
+               section.setLayoutData(EclipseUiUtils.fillAll());
+
+               Composite body = new Composite(section, SWT.NO_FOCUS);
+               section.setClient(body);
+               body.setLayoutData(EclipseUiUtils.fillAll());
+
+               LdifUsersTable userTableViewerCmp = createMemberPart(body, group);
+
+               SectionPart part = new GroupMembersPart(section, userTableViewerCmp);
+               getManagedForm().addPart(part);
+               addRemoveAbitily(part, userTableViewerCmp.getTableViewer(), group);
+       }
+
+       public LdifUsersTable createMemberPart(Composite parent, Group group) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+               // Define the displayed columns
+               List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+               columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24));
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+               columnDefs.add(new ColumnDefinition(new MailLP(), "Mail", 150));
+               // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name",
+               // 240));
+
+               // Create and configure the table
+               LdifUsersTable userViewerCmp = new MyUserTableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL,
+                               userAdminWrapper.getUserAdmin());
+
+               userViewerCmp.setColumnDefinitions(columnDefs);
+               userViewerCmp.populate(true, false);
+               userViewerCmp.setLayoutData(EclipseUiUtils.fillAll());
+
+               // Controllers
+               TableViewer userViewer = userViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener());
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               userViewer.addDropSupport(operations, tt,
+                               new GroupDropListener(userAdminWrapper, userViewerCmp, (Group) editor.getDisplayedUser()));
+
+               return userViewerCmp;
+       }
+
+       // Local viewers
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private final UserFilter userFilter;
+
+               public MyUserTableViewer(Composite parent, int style, UserAdmin userAdmin) {
+                       super(parent, style, true);
+                       userFilter = new UserFilter();
+
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       // reload user and set it in the editor
+                       Group group = (Group) editor.getDisplayedUser();
+                       Role[] roles = group.getMembers();
+                       List<User> users = new ArrayList<User>();
+                       userFilter.setSearchText(filter);
+                       // userFilter.setShowSystemRole(true);
+                       for (Role role : roles)
+                               // if (role.getType() == Role.GROUP)
+                               if (userFilter.select(null, null, role))
+                                       users.add((User) role);
+                       return users;
+               }
+       }
+
+       private void addRemoveAbitily(SectionPart sectionPart, TableViewer userViewer, Group group) {
+               Section section = sectionPart.getSection();
+               ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+               ToolBar toolbar = toolBarManager.createControl(section);
+               final Cursor handCursor = new Cursor(section.getDisplay(), SWT.CURSOR_HAND);
+               toolbar.setCursor(handCursor);
+               toolbar.addDisposeListener(new DisposeListener() {
+                       private static final long serialVersionUID = 3882131405820522925L;
+
+                       public void widgetDisposed(DisposeEvent e) {
+                               if ((handCursor != null) && (handCursor.isDisposed() == false)) {
+                                       handCursor.dispose();
+                               }
+                       }
+               });
+
+               Action action = new RemoveMembershipAction(userViewer, group, "Remove selected items from this group",
+                               SecurityAdminImages.ICON_REMOVE_DESC);
+               toolBarManager.add(action);
+               toolBarManager.update(true);
+               section.setTextClient(toolbar);
+       }
+
+       private class RemoveMembershipAction extends Action {
+               private static final long serialVersionUID = -1337713097184522588L;
+
+               private final TableViewer userViewer;
+               private final Group group;
+
+               RemoveMembershipAction(TableViewer userViewer, Group group, String name, ImageDescriptor img) {
+                       super(name, img);
+                       this.userViewer = userViewer;
+                       this.group = group;
+               }
+
+               @Override
+               public void run() {
+                       ISelection selection = userViewer.getSelection();
+                       if (selection.isEmpty())
+                               return;
+
+                       @SuppressWarnings("unchecked")
+                       Iterator<User> it = ((IStructuredSelection) selection).iterator();
+                       List<User> users = new ArrayList<User>();
+                       while (it.hasNext()) {
+                               User currUser = it.next();
+                               users.add(currUser);
+                       }
+
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       for (User user : users) {
+                               group.removeMember(user);
+                       }
+                       userAdminWrapper.commitOrNotifyTransactionStateChange();
+                       userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+               }
+       }
+
+       // LOCAL CONTROLLERS
+       private class GroupMembersPart extends SectionPart {
+               private final LdifUsersTable userViewer;
+               // private final Group group;
+
+               private GroupChangeListener listener;
+
+               public GroupMembersPart(Section section, LdifUsersTable userViewer) {
+                       super(section);
+                       this.userViewer = userViewer;
+                       // this.group = group;
+               }
+
+               @Override
+               public void initialize(IManagedForm form) {
+                       super.initialize(form);
+                       listener = editor.new GroupChangeListener(userViewer.getDisplay(), GroupMembersPart.this);
+                       userAdminWrapper.addListener(listener);
+               }
+
+               @Override
+               public void dispose() {
+                       userAdminWrapper.removeListener(listener);
+                       super.dispose();
+               }
+
+               @Override
+               public void refresh() {
+                       userViewer.refresh();
+                       super.refresh();
+               }
+       }
+
+       /**
+        * Defines this table as being a potential target to add group membership
+        * (roles) to this group
+        */
+       private class GroupDropListener extends ViewerDropAdapter {
+               private static final long serialVersionUID = 2893468717831451621L;
+
+               private final UserAdminWrapper userAdminWrapper;
+               // private final LdifUsersTable myUserViewerCmp;
+               private final Group myGroup;
+
+               public GroupDropListener(UserAdminWrapper userAdminWrapper, LdifUsersTable userTableViewerCmp, Group group) {
+                       super(userTableViewerCmp.getTableViewer());
+                       this.userAdminWrapper = userAdminWrapper;
+                       this.myGroup = group;
+                       // this.myUserViewerCmp = userTableViewerCmp;
+               }
+
+               @Override
+               public boolean validateDrop(Object target, int operation, TransferData transferType) {
+                       // Target is always OK in a list only view
+                       // TODO check if not a string
+                       boolean validDrop = true;
+                       return validDrop;
+               }
+
+               @Override
+               public void drop(DropTargetEvent event) {
+                       // TODO Is there an opportunity to perform the check before?
+                       String newUserName = (String) event.data;
+                       UserAdmin myUserAdmin = userAdminWrapper.getUserAdmin();
+                       Role role = myUserAdmin.getRole(newUserName);
+                       if (role.getType() == Role.GROUP) {
+                               Group newGroup = (Group) role;
+                               Shell shell = getViewer().getControl().getShell();
+                               // Sanity checks
+                               if (myGroup == newGroup) { // Equality
+                                       MessageDialog.openError(shell, "Forbidden addition ", "A group cannot be a member of itself.");
+                                       return;
+                               }
+
+                               // Cycle
+                               String myName = myGroup.getName();
+                               List<User> myMemberships = editor.getFlatGroups(myGroup);
+                               if (myMemberships.contains(newGroup)) {
+                                       MessageDialog.openError(shell, "Forbidden addition: cycle",
+                                                       "Cannot add " + newUserName + " to group " + myName + ". This would create a cycle");
+                                       return;
+                               }
+
+                               // Already member
+                               List<User> newGroupMemberships = editor.getFlatGroups(newGroup);
+                               if (newGroupMemberships.contains(myGroup)) {
+                                       MessageDialog.openError(shell, "Forbidden addition",
+                                                       "Cannot add " + newUserName + " to group " + myName + ", this membership already exists");
+                                       return;
+                               }
+                               userAdminWrapper.beginTransactionIfNeeded();
+                               myGroup.addMember(newGroup);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, myGroup));
+                       } else if (role.getType() == Role.USER) {
+                               // TODO check if the group is already member of this group
+                               UserTransaction transaction = userAdminWrapper.beginTransactionIfNeeded();
+                               User user = (User) role;
+                               myGroup.addMember(user);
+                               if (UserAdminWrapper.COMMIT_ON_SAVE)
+                                       try {
+                                               transaction.commit();
+                                       } catch (Exception e) {
+                                               throw new CmsException("Cannot commit transaction " + "after user group membership update", e);
+                                       }
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, myGroup));
+                       }
+                       super.drop(event);
+               }
+
+               @Override
+               public boolean performDrop(Object data) {
+                       // myUserViewerCmp.refresh();
+                       return true;
+               }
+       }
+
+       // LOCAL HELPERS
+       private Composite addSection(FormToolkit tk, Composite parent) {
+               Section section = tk.createSection(parent, SWT.NO_FOCUS);
+               section.setLayoutData(EclipseUiUtils.fillWidth());
+               Composite body = tk.createComposite(section, SWT.WRAP);
+               body.setLayoutData(EclipseUiUtils.fillAll());
+               section.setClient(body);
+               return body;
+       }
+
+       /** Creates label and text. */
+       // private Text createLT(Composite parent, String label, String value) {
+       // FormToolkit toolkit = getManagedForm().getToolkit();
+       // Label lbl = toolkit.createLabel(parent, label);
+       // lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+       // lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+       // Text text = toolkit.createText(parent, value, SWT.BORDER);
+       // text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+       // CmsUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+       // return text;
+       // }
+       //
+       Text createReadOnlyLT(Composite parent, String label, String value) {
+               FormToolkit toolkit = getManagedForm().getToolkit();
+               Label lbl = toolkit.createLabel(parent, label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+               Text text = toolkit.createText(parent, value, SWT.NONE);
+               text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               text.setEditable(false);
+               CmsUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+               return text;
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/GroupsView.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/GroupsView.java
new file mode 100644 (file)
index 0000000..453fa7d
--- /dev/null
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.parts;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.useradmin.UiUserAdminListener;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.CommonNameLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.DomainNameLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.RoleIconLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserDragListener;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserTableDefaultDClickListener;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.LdapObjs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.part.ViewPart;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** List all groups with filter */
+public class GroupsView extends ViewPart implements ArgeoNames {
+       private final static Log log = LogFactory.getLog(GroupsView.class);
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".groupsView";
+
+       /* DEPENDENCY INJECTION */
+       private UserAdminWrapper userAdminWrapper;
+
+       // UI Objects
+       private LdifUsersTable groupTableViewerCmp;
+       private TableViewer userViewer;
+       private List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+
+       private UserAdminListener listener;
+
+       @Override
+       public void createPartControl(Composite parent) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+//             boolean isAdmin = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+
+               // Define the displayed columns
+               columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 19));
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+               columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 100));
+               // Only show technical DN to admin
+               // if (isAdmin)
+               // columnDefs.add(new ColumnDefinition(new UserNameLP(),
+               // "Distinguished Name", 300));
+
+               // Create and configure the table
+               groupTableViewerCmp = new MyUserTableViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+
+               groupTableViewerCmp.setColumnDefinitions(columnDefs);
+//             if (isAdmin)
+//                     groupTableViewerCmp.populateWithStaticFilters(false, false);
+//             else
+                       groupTableViewerCmp.populate(true, false);
+
+               groupTableViewerCmp.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               // Links
+               userViewer = groupTableViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener());
+               getViewSite().setSelectionProvider(userViewer);
+
+               // Really?
+               groupTableViewerCmp.refresh();
+
+               // Drag and drop
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               userViewer.addDragSupport(operations, tt, new UserDragListener(userViewer));
+
+               // // Register a useradmin listener
+               // listener = new UserAdminListener() {
+               // @Override
+               // public void roleChanged(UserAdminEvent event) {
+               // if (userViewer != null && !userViewer.getTable().isDisposed())
+               // refresh();
+               // }
+               // };
+               // userAdminWrapper.addListener(listener);
+               // }
+
+               // Register a useradmin listener
+               listener = new MyUiUAListener(parent.getDisplay());
+               userAdminWrapper.addListener(listener);
+       }
+
+       private class MyUiUAListener extends UiUserAdminListener {
+               public MyUiUAListener(Display display) {
+                       super(display);
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       if (userViewer != null && !userViewer.getTable().isDisposed())
+                               refresh();
+               }
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private boolean showSystemRoles = true;
+
+               private final String[] knownProps = { LdapAttrs.uid.name(), LdapAttrs.cn.name(), LdapAttrs.DN };
+
+               public MyUserTableViewer(Composite parent, int style) {
+                       super(parent, style);
+                       showSystemRoles = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+               }
+
+               protected void populateStaticFilters(Composite staticFilterCmp) {
+                       staticFilterCmp.setLayout(new GridLayout());
+                       final Button showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK);
+                       showSystemRoleBtn.setText("Show system roles");
+                       showSystemRoles = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+                       showSystemRoleBtn.setSelection(showSystemRoles);
+
+                       showSystemRoleBtn.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = -7033424592697691676L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       showSystemRoles = showSystemRoleBtn.getSelection();
+                                       refresh();
+                               }
+
+                       });
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       Role[] roles;
+                       try {
+                               StringBuilder builder = new StringBuilder();
+                               StringBuilder tmpBuilder = new StringBuilder();
+                               if (EclipseUiUtils.notEmpty(filter))
+                                       for (String prop : knownProps) {
+                                               tmpBuilder.append("(");
+                                               tmpBuilder.append(prop);
+                                               tmpBuilder.append("=*");
+                                               tmpBuilder.append(filter);
+                                               tmpBuilder.append("*)");
+                                       }
+                               if (tmpBuilder.length() > 1) {
+                                       builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=")
+                                                       .append(LdapObjs.groupOfNames.name()).append(")");
+                                       if (!showSystemRoles)
+                                               builder.append("(!(").append(LdapAttrs.DN).append("=*").append(NodeConstants.ROLES_BASEDN)
+                                                               .append("))");
+                                       builder.append("(|");
+                                       builder.append(tmpBuilder.toString());
+                                       builder.append("))");
+                               } else {
+                                       if (!showSystemRoles)
+                                               builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=")
+                                                               .append(LdapObjs.groupOfNames.name()).append(")(!(").append(LdapAttrs.DN).append("=*")
+                                                               .append(NodeConstants.ROLES_BASEDN).append(")))");
+                                       else
+                                               builder.append("(").append(LdapAttrs.objectClass.name()).append("=")
+                                                               .append(LdapObjs.groupOfNames.name()).append(")");
+
+                               }
+                               roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString());
+                       } catch (InvalidSyntaxException e) {
+                               throw new CmsException("Unable to get roles with filter: " + filter, e);
+                       }
+                       List<User> users = new ArrayList<User>();
+                       for (Role role : roles)
+                               if (!users.contains(role))
+                                       users.add((User) role);
+                               else
+                                       log.warn("Duplicated role: " + role);
+
+                       return users;
+               }
+       }
+
+       public void refresh() {
+               groupTableViewerCmp.refresh();
+       }
+
+       // Override generic view methods
+       @Override
+       public void dispose() {
+               userAdminWrapper.removeListener(listener);
+               super.dispose();
+       }
+
+       @Override
+       public void setFocus() {
+               groupTableViewerCmp.setFocus();
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserBatchUpdateWizard.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserBatchUpdateWizard.java
new file mode 100644 (file)
index 0000000..7e3540f
--- /dev/null
@@ -0,0 +1,627 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.parts;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.transaction.SystemException;
+import javax.transaction.UserTransaction;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.workbench.internal.useradmin.UiAdminUtils;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.CommonNameLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.DomainNameLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.MailLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserNameLP;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.LdapObjs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.jface.dialogs.IPageChangeProvider;
+import org.eclipse.jface.dialogs.IPageChangedListener;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.PageChangedEvent;
+import org.eclipse.jface.wizard.IWizardContainer;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Wizard to update users */
+public class UserBatchUpdateWizard extends Wizard {
+
+       private final static Log log = LogFactory.getLog(UserBatchUpdateWizard.class);
+       private UserAdminWrapper userAdminWrapper;
+
+       // pages
+       private ChooseCommandWizardPage chooseCommandPage;
+       private ChooseUsersWizardPage userListPage;
+       private ValidateAndLaunchWizardPage validatePage;
+
+       // Various implemented commands keys
+       private final static String CMD_UPDATE_PASSWORD = "resetPassword";
+       private final static String CMD_UPDATE_EMAIL = "resetEmail";
+       private final static String CMD_GROUP_MEMBERSHIP = "groupMembership";
+
+       private final Map<String, String> commands = new HashMap<String, String>() {
+               private static final long serialVersionUID = 1L;
+               {
+                       put("Reset password(s)", CMD_UPDATE_PASSWORD);
+                       put("Reset email(s)", CMD_UPDATE_EMAIL);
+                       // TODO implement role / group management
+                       // put("Add/Remove from group", CMD_GROUP_MEMBERSHIP);
+               }
+       };
+
+       public UserBatchUpdateWizard(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+
+       @Override
+       public void addPages() {
+               chooseCommandPage = new ChooseCommandWizardPage();
+               addPage(chooseCommandPage);
+               userListPage = new ChooseUsersWizardPage();
+               addPage(userListPage);
+               validatePage = new ValidateAndLaunchWizardPage();
+               addPage(validatePage);
+       }
+
+       @Override
+       public boolean performFinish() {
+               if (!canFinish())
+                       return false;
+               UserTransaction ut = userAdminWrapper.getUserTransaction();
+               try {
+                       if (ut.getStatus() != javax.transaction.Status.STATUS_NO_TRANSACTION
+                                       && !MessageDialog.openConfirm(getShell(), "Existing Transaction",
+                                                       "A user transaction is already existing, " + "are you sure you want to proceed ?"))
+                               return false;
+               } catch (SystemException e) {
+                       throw new CmsException("Cannot get user transaction state " + "before user batch update", e);
+               }
+
+               // We cannot use jobs, user modifications are still meant to be done in
+               // the UIThread
+               // UpdateJob job = null;
+               // if (job != null)
+               // job.schedule();
+
+               if (CMD_UPDATE_PASSWORD.equals(chooseCommandPage.getCommand())) {
+                       char[] newValue = chooseCommandPage.getPwdValue();
+                       if (newValue == null)
+                               throw new CmsException("Password cannot be null or an empty string");
+                       ResetPassword job = new ResetPassword(userAdminWrapper, userListPage.getSelectedUsers(), newValue);
+                       job.doUpdate();
+               } else if (CMD_UPDATE_EMAIL.equals(chooseCommandPage.getCommand())) {
+                       String newValue = chooseCommandPage.getEmailValue();
+                       if (newValue == null)
+                               throw new CmsException("Password cannot be null or an empty string");
+                       ResetEmail job = new ResetEmail(userAdminWrapper, userListPage.getSelectedUsers(), newValue);
+                       job.doUpdate();
+               }
+               return true;
+       }
+
+       public boolean canFinish() {
+               if (this.getContainer().getCurrentPage() == validatePage)
+                       return true;
+               return false;
+       }
+
+       private class ResetPassword {
+               private char[] newPwd;
+               private UserAdminWrapper userAdminWrapper;
+               private List<User> usersToUpdate;
+
+               public ResetPassword(UserAdminWrapper userAdminWrapper, List<User> usersToUpdate, char[] newPwd) {
+                       this.newPwd = newPwd;
+                       this.usersToUpdate = usersToUpdate;
+                       this.userAdminWrapper = userAdminWrapper;
+               }
+
+               @SuppressWarnings("unchecked")
+               protected void doUpdate() {
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       try {
+                               for (User user : usersToUpdate) {
+                                       // the char array is emptied after being used.
+                                       user.getCredentials().put(null, newPwd.clone());
+                               }
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                       } catch (Exception e) {
+                               throw new CmsException("Cannot perform batch update on users", e);
+                       } finally {
+                               UserTransaction ut = userAdminWrapper.getUserTransaction();
+                               try {
+                                       if (ut.getStatus() != javax.transaction.Status.STATUS_NO_TRANSACTION)
+                                               ut.rollback();
+                               } catch (IllegalStateException | SecurityException | SystemException e) {
+                                       log.error("Unable to rollback session in 'finally', " + "the system might be in a dirty state");
+                                       e.printStackTrace();
+                               }
+                       }
+               }
+       }
+
+       private class ResetEmail {
+               private String newEmail;
+               private UserAdminWrapper userAdminWrapper;
+               private List<User> usersToUpdate;
+
+               public ResetEmail(UserAdminWrapper userAdminWrapper, List<User> usersToUpdate, String newEmail) {
+                       this.newEmail = newEmail;
+                       this.usersToUpdate = usersToUpdate;
+                       this.userAdminWrapper = userAdminWrapper;
+               }
+
+               @SuppressWarnings("unchecked")
+               protected void doUpdate() {
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       try {
+                               for (User user : usersToUpdate) {
+                                       // the char array is emptied after being used.
+                                       user.getProperties().put(LdapAttrs.mail.name(), newEmail);
+                               }
+
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               if (!usersToUpdate.isEmpty())
+                                       userAdminWrapper.notifyListeners(
+                                                       new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, usersToUpdate.get(0)));
+                       } catch (Exception e) {
+                               throw new CmsException("Cannot perform batch update on users", e);
+                       } finally {
+                               UserTransaction ut = userAdminWrapper.getUserTransaction();
+                               try {
+                                       if (ut.getStatus() != javax.transaction.Status.STATUS_NO_TRANSACTION)
+                                               ut.rollback();
+                               } catch (IllegalStateException | SecurityException | SystemException e) {
+                                       log.error("Unable to rollback session in finally block, the system might be in a dirty state");
+                                       e.printStackTrace();
+                               }
+                       }
+               }
+       }
+
+       // @SuppressWarnings("unused")
+       // private class AddToGroup extends UpdateJob {
+       // private String groupID;
+       // private Session session;
+       //
+       // public AddToGroup(Session session, List<Node> nodesToUpdate,
+       // String groupID) {
+       // super(session, nodesToUpdate);
+       // this.session = session;
+       // this.groupID = groupID;
+       // }
+       //
+       // protected void doUpdate(Node node) {
+       // log.info("Add/Remove to group actions are not yet implemented");
+       // // TODO implement this
+       // // try {
+       // // throw new CmsException("Not yet implemented");
+       // // } catch (RepositoryException re) {
+       // // throw new CmsException(
+       // // "Unable to update boolean value for node " + node, re);
+       // // }
+       // }
+       // }
+
+       // /**
+       // * Base privileged job that will be run asynchronously to perform the
+       // batch
+       // * update
+       // */
+       // private abstract class UpdateJob extends PrivilegedJob {
+       //
+       // private final UserAdminWrapper userAdminWrapper;
+       // private final List<User> usersToUpdate;
+       //
+       // protected abstract void doUpdate(User user);
+       //
+       // public UpdateJob(UserAdminWrapper userAdminWrapper,
+       // List<User> usersToUpdate) {
+       // super("Perform update");
+       // this.usersToUpdate = usersToUpdate;
+       // this.userAdminWrapper = userAdminWrapper;
+       // }
+       //
+       // @Override
+       // protected IStatus doRun(IProgressMonitor progressMonitor) {
+       // try {
+       // JcrMonitor monitor = new EclipseJcrMonitor(progressMonitor);
+       // int total = usersToUpdate.size();
+       // monitor.beginTask("Performing change", total);
+       // userAdminWrapper.beginTransactionIfNeeded();
+       // for (User user : usersToUpdate) {
+       // doUpdate(user);
+       // monitor.worked(1);
+       // }
+       // userAdminWrapper.getUserTransaction().commit();
+       // } catch (Exception e) {
+       // throw new CmsException(
+       // "Cannot perform batch update on users", e);
+       // } finally {
+       // UserTransaction ut = userAdminWrapper.getUserTransaction();
+       // try {
+       // if (ut.getStatus() != javax.transaction.Status.STATUS_NO_TRANSACTION)
+       // ut.rollback();
+       // } catch (IllegalStateException | SecurityException
+       // | SystemException e) {
+       // log.error("Unable to rollback session in 'finally', "
+       // + "the system might be in a dirty state");
+       // e.printStackTrace();
+       // }
+       // }
+       // return Status.OK_STATUS;
+       // }
+       // }
+
+       // PAGES
+       /**
+        * Displays a combo box that enables user to choose which action to perform
+        */
+       private class ChooseCommandWizardPage extends WizardPage {
+               private static final long serialVersionUID = -8069434295293996633L;
+               private Combo chooseCommandCmb;
+               private Button trueChk;
+               private Text valueTxt;
+               private Text pwdTxt;
+               private Text pwd2Txt;
+
+               public ChooseCommandWizardPage() {
+                       super("Choose a command to run.");
+                       setTitle("Choose a command to run.");
+               }
+
+               @Override
+               public void createControl(Composite parent) {
+                       GridLayout gl = new GridLayout();
+                       Composite container = new Composite(parent, SWT.NO_FOCUS);
+                       container.setLayout(gl);
+
+                       chooseCommandCmb = new Combo(container, SWT.READ_ONLY);
+                       chooseCommandCmb.setLayoutData(EclipseUiUtils.fillWidth());
+                       String[] values = commands.keySet().toArray(new String[0]);
+                       chooseCommandCmb.setItems(values);
+
+                       final Composite bottomPart = new Composite(container, SWT.NO_FOCUS);
+                       bottomPart.setLayoutData(EclipseUiUtils.fillAll());
+                       bottomPart.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       chooseCommandCmb.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       if (getCommand().equals(CMD_UPDATE_PASSWORD))
+                                               populatePasswordCmp(bottomPart);
+                                       else if (getCommand().equals(CMD_UPDATE_EMAIL))
+                                               populateEmailCmp(bottomPart);
+                                       else if (getCommand().equals(CMD_GROUP_MEMBERSHIP))
+                                               populateGroupCmp(bottomPart);
+                                       else
+                                               populateBooleanFlagCmp(bottomPart);
+                                       checkPageComplete();
+                                       bottomPart.layout(true, true);
+                               }
+                       });
+                       setControl(container);
+               }
+
+               private void populateBooleanFlagCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       trueChk = new Button(parent, SWT.CHECK);
+                       trueChk.setText("Do it. (It will to the contrary if unchecked)");
+                       trueChk.setSelection(true);
+                       trueChk.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false));
+               }
+
+               private void populatePasswordCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       Composite body = new Composite(parent, SWT.NO_FOCUS);
+
+                       ModifyListener ml = new ModifyListener() {
+                               private static final long serialVersionUID = -1558726363536729634L;
+
+                               @Override
+                               public void modifyText(ModifyEvent event) {
+                                       checkPageComplete();
+                               }
+                       };
+
+                       body.setLayout(new GridLayout(2, false));
+                       body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       pwdTxt = EclipseUiUtils.createGridLP(body, "New password", ml);
+                       pwd2Txt = EclipseUiUtils.createGridLP(body, "Repeat password", ml);
+               }
+
+               private void populateEmailCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       Composite body = new Composite(parent, SWT.NO_FOCUS);
+
+                       ModifyListener ml = new ModifyListener() {
+                               private static final long serialVersionUID = 2147704227294268317L;
+
+                               @Override
+                               public void modifyText(ModifyEvent event) {
+                                       checkPageComplete();
+                               }
+                       };
+
+                       body.setLayout(new GridLayout(2, false));
+                       body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       valueTxt = EclipseUiUtils.createGridLT(body, "New e-mail", ml);
+               }
+
+               private void checkPageComplete() {
+                       String errorMsg = null;
+                       if (chooseCommandCmb.getSelectionIndex() < 0)
+                               errorMsg = "Please select an action";
+                       else if (CMD_UPDATE_EMAIL.equals(getCommand())) {
+                               if (!valueTxt.getText().matches(UiAdminUtils.EMAIL_PATTERN))
+                                       errorMsg = "Not a valid e-mail address";
+                       } else if (CMD_UPDATE_PASSWORD.equals(getCommand())) {
+                               if (EclipseUiUtils.isEmpty(pwdTxt.getText()) || pwdTxt.getText().length() < 4)
+                                       errorMsg = "Please enter a password that is at least 4 character long";
+                               else if (!pwdTxt.getText().equals(pwd2Txt.getText()))
+                                       errorMsg = "Passwords are different";
+                       }
+                       if (EclipseUiUtils.notEmpty(errorMsg)) {
+                               setMessage(errorMsg, WizardPage.ERROR);
+                               setPageComplete(false);
+                       } else {
+                               setMessage("Page complete, you can proceed to user choice", WizardPage.INFORMATION);
+                               setPageComplete(true);
+                       }
+
+                       getContainer().updateButtons();
+               }
+
+               private void populateGroupCmp(Composite parent) {
+                       EclipseUiUtils.clear(parent);
+                       trueChk = new Button(parent, SWT.CHECK);
+                       trueChk.setText("Add to group. (It will remove user(s) from the " + "corresponding group if unchecked)");
+                       trueChk.setSelection(true);
+                       trueChk.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false));
+               }
+
+               protected String getCommand() {
+                       return commands.get(chooseCommandCmb.getItem(chooseCommandCmb.getSelectionIndex()));
+               }
+
+               protected String getCommandLbl() {
+                       return chooseCommandCmb.getItem(chooseCommandCmb.getSelectionIndex());
+               }
+
+               @SuppressWarnings("unused")
+               protected boolean getBoleanValue() {
+                       // FIXME this is not consistent and will lead to errors.
+                       if ("argeo:enabled".equals(getCommand()))
+                               return trueChk.getSelection();
+                       else
+                               return !trueChk.getSelection();
+               }
+
+               @SuppressWarnings("unused")
+               protected String getStringValue() {
+                       String value = null;
+                       if (valueTxt != null) {
+                               value = valueTxt.getText();
+                               if ("".equals(value.trim()))
+                                       value = null;
+                       }
+                       return value;
+               }
+
+               protected char[] getPwdValue() {
+                       // We do not directly reset the password text fields: There is no
+                       // need to over secure this process: setting a pwd to multi users
+                       // at the same time is anyhow a bad practice and should be used only
+                       // in test environment or for temporary access
+                       if (pwdTxt == null || pwdTxt.isDisposed())
+                               return null;
+                       else
+                               return pwdTxt.getText().toCharArray();
+               }
+
+               protected String getEmailValue() {
+                       // We do not directly reset the password text fields: There is no
+                       // need to over secure this process: setting a pwd to multi users
+                       // at the same time is anyhow a bad practice and should be used only
+                       // in test environment or for temporary access
+                       if (valueTxt == null || valueTxt.isDisposed())
+                               return null;
+                       else
+                               return valueTxt.getText();
+               }
+       }
+
+       /**
+        * Displays a list of users with a check box to be able to choose some of
+        * them
+        */
+       private class ChooseUsersWizardPage extends WizardPage implements IPageChangedListener {
+               private static final long serialVersionUID = 7651807402211214274L;
+               private ChooseUserTableViewer userTableCmp;
+
+               public ChooseUsersWizardPage() {
+                       super("Choose Users");
+                       setTitle("Select users who will be impacted");
+               }
+
+               @Override
+               public void createControl(Composite parent) {
+                       Composite pageCmp = new Composite(parent, SWT.NONE);
+                       pageCmp.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       // Define the displayed columns
+                       List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+                       columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150));
+                       columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150));
+                       columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200));
+
+                       // Only show technical DN to admin
+                       if (CurrentUser.isInRole(NodeConstants.ROLE_ADMIN))
+                               columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300));
+
+                       userTableCmp = new ChooseUserTableViewer(pageCmp, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+                       userTableCmp.setLayoutData(EclipseUiUtils.fillAll());
+                       userTableCmp.setColumnDefinitions(columnDefs);
+                       userTableCmp.populate(true, true);
+                       userTableCmp.refresh();
+
+                       setControl(pageCmp);
+
+                       // Add listener to update message when shown
+                       final IWizardContainer wContainer = this.getContainer();
+                       if (wContainer instanceof IPageChangeProvider) {
+                               ((IPageChangeProvider) wContainer).addPageChangedListener(this);
+                       }
+
+               }
+
+               @Override
+               public void pageChanged(PageChangedEvent event) {
+                       if (event.getSelectedPage() == this) {
+                               String msg = "Chosen batch action: " + chooseCommandPage.getCommandLbl();
+                               ((WizardPage) event.getSelectedPage()).setMessage(msg);
+                       }
+               }
+
+               protected List<User> getSelectedUsers() {
+                       return userTableCmp.getSelectedUsers();
+               }
+
+               private class ChooseUserTableViewer extends LdifUsersTable {
+                       private static final long serialVersionUID = 5080437561015853124L;
+                       private final String[] knownProps = { LdapAttrs.uid.name(), LdapAttrs.DN, LdapAttrs.cn.name(),
+                                       LdapAttrs.givenName.name(), LdapAttrs.sn.name(), LdapAttrs.mail.name() };
+
+                       public ChooseUserTableViewer(Composite parent, int style) {
+                               super(parent, style);
+                       }
+
+                       @Override
+                       protected List<User> listFilteredElements(String filter) {
+                               Role[] roles;
+
+                               try {
+                                       StringBuilder builder = new StringBuilder();
+
+                                       StringBuilder tmpBuilder = new StringBuilder();
+                                       if (EclipseUiUtils.notEmpty(filter))
+                                               for (String prop : knownProps) {
+                                                       tmpBuilder.append("(");
+                                                       tmpBuilder.append(prop);
+                                                       tmpBuilder.append("=*");
+                                                       tmpBuilder.append(filter);
+                                                       tmpBuilder.append("*)");
+                                               }
+                                       if (tmpBuilder.length() > 1) {
+                                               builder.append("(&(").append(LdapAttrs.objectClass.name()).append("=")
+                                                               .append(LdapObjs.inetOrgPerson.name()).append(")(|");
+                                               builder.append(tmpBuilder.toString());
+                                               builder.append("))");
+                                       } else
+                                               builder.append("(").append(LdapAttrs.objectClass.name()).append("=")
+                                                               .append(LdapObjs.inetOrgPerson.name()).append(")");
+                                       roles = userAdminWrapper.getUserAdmin().getRoles(builder.toString());
+                               } catch (InvalidSyntaxException e) {
+                                       throw new CmsException("Unable to get roles with filter: " + filter, e);
+                               }
+                               List<User> users = new ArrayList<User>();
+                               for (Role role : roles)
+                                       // Prevent current logged in user to perform batch on
+                                       // himself
+                                       if (!UserAdminUtils.isCurrentUser((User) role))
+                                               users.add((User) role);
+                               return users;
+                       }
+               }
+       }
+
+       /** Summary of input data before launching the process */
+       private class ValidateAndLaunchWizardPage extends WizardPage implements IPageChangedListener {
+               private static final long serialVersionUID = 7098918351451743853L;
+               private ChosenUsersTableViewer userTableCmp;
+
+               public ValidateAndLaunchWizardPage() {
+                       super("Validate and launch");
+                       setTitle("Validate and launch");
+               }
+
+               @Override
+               public void createControl(Composite parent) {
+                       Composite pageCmp = new Composite(parent, SWT.NO_FOCUS);
+                       pageCmp.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+                       columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name", 150));
+                       columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150));
+                       columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200));
+                       // Only show technical DN to admin
+                       if (CurrentUser.isInRole(NodeConstants.ROLE_ADMIN))
+                               columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name", 300));
+                       userTableCmp = new ChosenUsersTableViewer(pageCmp, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+                       userTableCmp.setLayoutData(EclipseUiUtils.fillAll());
+                       userTableCmp.setColumnDefinitions(columnDefs);
+                       userTableCmp.populate(false, false);
+                       userTableCmp.refresh();
+                       setControl(pageCmp);
+                       // Add listener to update message when shown
+                       final IWizardContainer wContainer = this.getContainer();
+                       if (wContainer instanceof IPageChangeProvider) {
+                               ((IPageChangeProvider) wContainer).addPageChangedListener(this);
+                       }
+               }
+
+               @Override
+               public void pageChanged(PageChangedEvent event) {
+                       if (event.getSelectedPage() == this) {
+                               @SuppressWarnings({ "unchecked", "rawtypes" })
+                               Object[] values = ((ArrayList) userListPage.getSelectedUsers())
+                                               .toArray(new Object[userListPage.getSelectedUsers().size()]);
+                               userTableCmp.getTableViewer().setInput(values);
+                               String msg = "Following batch action: [" + chooseCommandPage.getCommandLbl()
+                                               + "] will be perfomed on the users listed below.\n";
+                               // + "Are you sure you want to proceed?";
+                               setMessage(msg);
+                       }
+               }
+
+               private class ChosenUsersTableViewer extends LdifUsersTable {
+                       private static final long serialVersionUID = 7814764735794270541L;
+
+                       public ChosenUsersTableViewer(Composite parent, int style) {
+                               super(parent, style);
+                       }
+
+                       @Override
+                       protected List<User> listFilteredElements(String filter) {
+                               return userListPage.getSelectedUsers();
+                       }
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserEditor.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserEditor.java
new file mode 100644 (file)
index 0000000..a1e5c62
--- /dev/null
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.parts;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Repository;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.useradmin.UiUserAdminListener;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeInstance;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.forms.AbstractFormPart;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Editor for a user, might be a user or a group. */
+public class UserEditor extends FormEditor {
+       private static final long serialVersionUID = 8357851520380820241L;
+
+       public final static String USER_EDITOR_ID = WorkbenchUiPlugin.PLUGIN_ID + ".userEditor";
+       public final static String GROUP_EDITOR_ID = WorkbenchUiPlugin.PLUGIN_ID + ".groupEditor";
+
+       /* DEPENDENCY INJECTION */
+       private Repository repository;
+       private UserAdminWrapper userAdminWrapper;
+       private UserAdmin userAdmin;
+       private NodeInstance nodeInstance;
+
+       // Context
+       private User user;
+       private String username;
+
+       private NameChangeListener listener;
+
+       public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+               super.init(site, input);
+               this.userAdmin = userAdminWrapper.getUserAdmin();
+               username = ((UserEditorInput) getEditorInput()).getUsername();
+               user = (User) userAdmin.getRole(username);
+
+               listener = new NameChangeListener(site.getShell().getDisplay());
+               userAdminWrapper.addListener(listener);
+               updateEditorTitle(null);
+       }
+
+       /**
+        * returns the list of all authorization for the given user or of the current
+        * displayed user if parameter is null
+        */
+       protected List<User> getFlatGroups(User aUser) {
+               Authorization currAuth;
+               if (aUser == null)
+                       currAuth = userAdmin.getAuthorization(this.user);
+               else
+                       currAuth = userAdmin.getAuthorization(aUser);
+
+               String[] roles = currAuth.getRoles();
+
+               List<User> groups = new ArrayList<User>();
+               for (String roleStr : roles) {
+                       User currRole = (User) userAdmin.getRole(roleStr);
+                       if (currRole != null && !groups.contains(currRole))
+                               groups.add(currRole);
+               }
+               return groups;
+       }
+
+       /** Exposes the user (or group) that is displayed by the current editor */
+       protected User getDisplayedUser() {
+               return user;
+       }
+
+       private void setDisplayedUser(User user) {
+               this.user = user;
+       }
+
+       void updateEditorTitle(String title) {
+               if (title == null) {
+                       String commonName = UserAdminUtils.getProperty(user, LdapAttrs.cn.name());
+                       title = "".equals(commonName) ? user.getName() : commonName;
+               }
+               setPartName(title);
+       }
+
+       protected void addPages() {
+               try {
+                       if (user.getType() == Role.GROUP)
+                               addPage(new GroupMainPage(this, userAdminWrapper, repository, nodeInstance));
+                       else
+                               addPage(new UserMainPage(this, userAdminWrapper));
+               } catch (Exception e) {
+                       throw new CmsException("Cannot add pages", e);
+               }
+       }
+
+       @Override
+       public void doSave(IProgressMonitor monitor) {
+               userAdminWrapper.beginTransactionIfNeeded();
+               commitPages(true);
+               userAdminWrapper.commitOrNotifyTransactionStateChange();
+               firePropertyChange(PROP_DIRTY);
+               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_REMOVED, user));
+       }
+
+       @Override
+       public void doSaveAs() {
+       }
+
+       @Override
+       public boolean isSaveAsAllowed() {
+               return false;
+       }
+
+       @Override
+       public void dispose() {
+               userAdminWrapper.removeListener(listener);
+               super.dispose();
+       }
+
+       // CONTROLERS FOR THIS EDITOR AND ITS PAGES
+
+       private class NameChangeListener extends UiUserAdminListener {
+               public NameChangeListener(Display display) {
+                       super(display);
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       Role changedRole = event.getRole();
+                       if (changedRole == null || changedRole.equals(user)) {
+                               updateEditorTitle(null);
+                               User reloadedUser = (User) userAdminWrapper.getUserAdmin().getRole(user.getName());
+                               setDisplayedUser(reloadedUser);
+                       }
+               }
+       }
+
+       class MainInfoListener extends UiUserAdminListener {
+               private final AbstractFormPart part;
+
+               public MainInfoListener(Display display, AbstractFormPart part) {
+                       super(display);
+                       this.part = part;
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       // Rollback
+                       if (event.getRole() == null)
+                               part.markStale();
+               }
+       }
+
+       class GroupChangeListener extends UiUserAdminListener {
+               private final AbstractFormPart part;
+
+               public GroupChangeListener(Display display, AbstractFormPart part) {
+                       super(display);
+                       this.part = part;
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       // always mark as stale
+                       part.markStale();
+               }
+       }
+
+       /** Registers a listener that will notify this part */
+       class FormPartML implements ModifyListener {
+               private static final long serialVersionUID = 6299808129505381333L;
+               private AbstractFormPart formPart;
+
+               public FormPartML(AbstractFormPart generalPart) {
+                       this.formPart = generalPart;
+               }
+
+               public void modifyText(ModifyEvent e) {
+                       // Discard event when the control does not have the focus, typically
+                       // to avoid all editors being marked as dirty during a Rollback
+                       if (((Control) e.widget).isFocusControl())
+                               formPart.markDirty();
+               }
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setNodeInstance(NodeInstance nodeInstance) {
+               this.nodeInstance = nodeInstance;
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserEditorInput.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserEditorInput.java
new file mode 100644 (file)
index 0000000..1e7ee4b
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.parts;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IPersistableElement;
+
+/**
+ * Editor input for an user defined by unique name (usually a distinguished
+ * name).
+ */
+public class UserEditorInput implements IEditorInput {
+       private final String username;
+
+       public UserEditorInput(String username) {
+               this.username = username;
+       }
+
+       @SuppressWarnings("unchecked")
+       public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
+               return null;
+       }
+
+       public boolean exists() {
+               return username != null;
+       }
+
+       public ImageDescriptor getImageDescriptor() {
+               return null;
+       }
+
+       public String getName() {
+               return username != null ? username : "<new user>";
+       }
+
+       public IPersistableElement getPersistable() {
+               return null;
+       }
+
+       public String getToolTipText() {
+               return username != null ? username : "<new user>";
+       }
+
+       public boolean equals(Object obj) {
+               if (!(obj instanceof UserEditorInput))
+                       return false;
+               if (((UserEditorInput) obj).getUsername() == null)
+                       return false;
+               return ((UserEditorInput) obj).getUsername().equals(username);
+       }
+
+       public String getUsername() {
+               return username;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserMainPage.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UserMainPage.java
new file mode 100644 (file)
index 0000000..04111c4
--- /dev/null
@@ -0,0 +1,571 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.parts;
+
+import static org.argeo.cms.util.UserAdminUtils.getProperty;
+import static org.argeo.naming.LdapAttrs.cn;
+import static org.argeo.naming.LdapAttrs.givenName;
+import static org.argeo.naming.LdapAttrs.mail;
+import static org.argeo.naming.LdapAttrs.sn;
+import static org.argeo.naming.LdapAttrs.uid;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.workbench.CmsWorkbenchStyles;
+import org.argeo.cms.ui.workbench.internal.useradmin.SecurityAdminImages;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UserEditor.GroupChangeListener;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UserEditor.MainInfoListener;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.CommonNameLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.DomainNameLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.RoleIconLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserFilter;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserTableDefaultDClickListener;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TrayDialog;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerDropAdapter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTargetEvent;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.dnd.TransferData;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Cursor;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.ui.forms.AbstractFormPart;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.SectionPart;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+import org.eclipse.ui.forms.widgets.Section;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/** Display/edit the properties of a given user */
+public class UserMainPage extends FormPage implements ArgeoNames {
+       final static String ID = "UserEditor.mainPage";
+
+       private final UserEditor editor;
+       private UserAdminWrapper userAdminWrapper;
+
+       // Local configuration
+       private final int PRE_TITLE_INDENT = 10;
+
+       public UserMainPage(FormEditor editor, UserAdminWrapper userAdminWrapper) {
+               super(editor, ID, "Main");
+               this.editor = (UserEditor) editor;
+               this.userAdminWrapper = userAdminWrapper;
+       }
+
+       protected void createFormContent(final IManagedForm mf) {
+               ScrolledForm form = mf.getForm();
+               Composite body = form.getBody();
+               GridLayout mainLayout = new GridLayout();
+               // mainLayout.marginRight = 10;
+               body.setLayout(mainLayout);
+               User user = editor.getDisplayedUser();
+               appendOverviewPart(body, user);
+               // Remove to ability to force the password for his own user. The user
+               // must then use the change pwd feature
+               appendMemberOfPart(body, user);
+       }
+
+       /** Creates the general section */
+       private void appendOverviewPart(final Composite parent, final User user) {
+               FormToolkit tk = getManagedForm().getToolkit();
+
+               Section section = tk.createSection(parent, SWT.NO_FOCUS);
+               GridData gd = EclipseUiUtils.fillWidth();
+               // gd.verticalAlignment = PRE_TITLE_INDENT;
+               section.setLayoutData(gd);
+               Composite body = tk.createComposite(section, SWT.WRAP);
+               body.setLayoutData(EclipseUiUtils.fillAll());
+               section.setClient(body);
+               // body.setLayout(new GridLayout(6, false));
+               body.setLayout(new GridLayout(2, false));
+
+               Text commonName = createReadOnlyLT(tk, body, "Name", getProperty(user, cn));
+               Text distinguishedName = createReadOnlyLT(tk, body, "Login", getProperty(user, uid));
+               Text firstName = createLT(tk, body, "First name", getProperty(user, givenName));
+               Text lastName = createLT(tk, body, "Last name", getProperty(user, sn));
+               Text email = createLT(tk, body, "Email", getProperty(user, mail));
+
+               Link resetPwdLk = new Link(body, SWT.NONE);
+               if (!UserAdminUtils.isCurrentUser(user)) {
+                       resetPwdLk.setText("<a>Reset password</a>");
+               }
+               resetPwdLk.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1));
+
+               // create form part (controller)
+               AbstractFormPart part = new SectionPart((Section) body.getParent()) {
+                       private MainInfoListener listener;
+
+                       @Override
+                       public void initialize(IManagedForm form) {
+                               super.initialize(form);
+                               listener = editor.new MainInfoListener(parent.getDisplay(), this);
+                               userAdminWrapper.addListener(listener);
+                       }
+
+                       @Override
+                       public void dispose() {
+                               userAdminWrapper.removeListener(listener);
+                               super.dispose();
+                       }
+
+                       @SuppressWarnings("unchecked")
+                       public void commit(boolean onSave) {
+                               // TODO Sanity checks (mail validity...)
+                               user.getProperties().put(LdapAttrs.givenName.name(), firstName.getText());
+                               user.getProperties().put(LdapAttrs.sn.name(), lastName.getText());
+                               user.getProperties().put(LdapAttrs.cn.name(), commonName.getText());
+                               user.getProperties().put(LdapAttrs.mail.name(), email.getText());
+                               super.commit(onSave);
+                       }
+
+                       @Override
+                       public void refresh() {
+                               distinguishedName.setText(UserAdminUtils.getProperty(user, LdapAttrs.uid.name()));
+                               commonName.setText(UserAdminUtils.getProperty(user, LdapAttrs.cn.name()));
+                               firstName.setText(UserAdminUtils.getProperty(user, LdapAttrs.givenName.name()));
+                               lastName.setText(UserAdminUtils.getProperty(user, LdapAttrs.sn.name()));
+                               email.setText(UserAdminUtils.getProperty(user, LdapAttrs.mail.name()));
+                               refreshFormTitle(user);
+                               super.refresh();
+                       }
+               };
+
+               // Improve this: automatically generate CN when first or last name
+               // changes
+               ModifyListener cnML = new ModifyListener() {
+                       private static final long serialVersionUID = 4298649222869835486L;
+
+                       @Override
+                       public void modifyText(ModifyEvent event) {
+                               String first = firstName.getText();
+                               String last = lastName.getText();
+                               String cn = first.trim() + " " + last.trim() + " ";
+                               cn = cn.trim();
+                               commonName.setText(cn);
+                               // getManagedForm().getForm().setText(cn);
+                               editor.updateEditorTitle(cn);
+                       }
+               };
+               firstName.addModifyListener(cnML);
+               lastName.addModifyListener(cnML);
+
+               ModifyListener defaultListener = editor.new FormPartML(part);
+               firstName.addModifyListener(defaultListener);
+               lastName.addModifyListener(defaultListener);
+               email.addModifyListener(defaultListener);
+
+               if (!UserAdminUtils.isCurrentUser(user))
+                       resetPwdLk.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = 5881800534589073787L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       new ChangePasswordDialog(tk, user, "Reset password").open();
+                               }
+                       });
+
+               getManagedForm().addPart(part);
+       }
+
+       private class ChangePasswordDialog extends TrayDialog {
+               private static final long serialVersionUID = 2843538207460082349L;
+
+               private User user;
+               private Text password1;
+               private Text password2;
+               private String title;
+               private FormToolkit tk;
+
+               public ChangePasswordDialog(FormToolkit tk, User user, String title) {
+                       super(Display.getDefault().getActiveShell());
+                       this.tk = tk;
+                       this.user = user;
+                       this.title = title;
+               }
+
+               protected Control createDialogArea(Composite parent) {
+                       Composite dialogarea = (Composite) super.createDialogArea(parent);
+                       dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       Composite body = new Composite(dialogarea, SWT.NO_FOCUS);
+                       body.setLayoutData(EclipseUiUtils.fillAll());
+                       GridLayout layout = new GridLayout(2, false);
+                       body.setLayout(layout);
+
+                       password1 = createLP(tk, body, "New password", "");
+                       password2 = createLP(tk, body, "Repeat password", "");
+                       parent.pack();
+                       return body;
+               }
+
+               @SuppressWarnings("unchecked")
+               @Override
+               protected void okPressed() {
+                       String msg = null;
+
+                       if (password1.getText().equals(""))
+                               msg = "Password cannot be empty";
+                       else if (password1.getText().equals(password2.getText())) {
+                               char[] newPassword = password1.getText().toCharArray();
+                               // userAdminWrapper.beginTransactionIfNeeded();
+                               userAdminWrapper.beginTransactionIfNeeded();
+                               user.getCredentials().put(null, newPassword);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               super.okPressed();
+                       } else {
+                               msg = "Passwords are not equals";
+                       }
+
+                       if (EclipseUiUtils.notEmpty(msg))
+                               MessageDialog.openError(getParentShell(), "Cannot reset pasword", msg);
+               }
+
+               protected void configureShell(Shell shell) {
+                       super.configureShell(shell);
+                       shell.setText(title);
+               }
+       }
+
+       private LdifUsersTable appendMemberOfPart(final Composite parent, User user) {
+               FormToolkit tk = getManagedForm().getToolkit();
+               Section section = addSection(tk, parent, "Roles");
+               Composite body = (Composite) section.getClient();
+               body.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+               // boolean isAdmin = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+
+               // Displayed columns
+               List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+               columnDefs.add(new ColumnDefinition(new RoleIconLP(), "", 0, 24));
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Name", 150));
+               columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 100));
+               // Only show technical DN to administrators
+               // if (isAdmin)
+               // columnDefs.add(new ColumnDefinition(new UserNameLP(), "Distinguished Name",
+               // 300));
+
+               // Create and configure the table
+               final LdifUsersTable userViewerCmp = new MyUserTableViewer(body, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL, user);
+
+               userViewerCmp.setColumnDefinitions(columnDefs);
+               // if (isAdmin)
+               // userViewerCmp.populateWithStaticFilters(false, false);
+               // else
+               userViewerCmp.populate(true, false);
+               GridData gd = EclipseUiUtils.fillAll();
+               gd.heightHint = 500;
+               userViewerCmp.setLayoutData(gd);
+
+               // Controllers
+               TableViewer userViewer = userViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener());
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               GroupDropListener dropL = new GroupDropListener(userAdminWrapper, userViewer, user);
+               userViewer.addDropSupport(operations, tt, dropL);
+
+               SectionPart part = new SectionPart((Section) body.getParent()) {
+
+                       private GroupChangeListener listener;
+
+                       @Override
+                       public void initialize(IManagedForm form) {
+                               super.initialize(form);
+                               listener = editor.new GroupChangeListener(parent.getDisplay(), this);
+                               userAdminWrapper.addListener(listener);
+                       }
+
+                       public void commit(boolean onSave) {
+                               super.commit(onSave);
+                       }
+
+                       @Override
+                       public void dispose() {
+                               userAdminWrapper.removeListener(listener);
+                               super.dispose();
+                       }
+
+                       @Override
+                       public void refresh() {
+                               userViewerCmp.refresh();
+                               super.refresh();
+                       }
+               };
+               getManagedForm().addPart(part);
+               addRemoveAbitily(part, userViewer, user);
+               return userViewerCmp;
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 2653790051461237329L;
+
+               private Button showSystemRoleBtn;
+
+               private final User user;
+               private final UserFilter userFilter;
+
+               public MyUserTableViewer(Composite parent, int style, User user) {
+                       super(parent, style, true);
+                       this.user = user;
+                       userFilter = new UserFilter();
+               }
+
+               protected void populateStaticFilters(Composite staticFilterCmp) {
+                       staticFilterCmp.setLayout(new GridLayout());
+                       showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK);
+                       showSystemRoleBtn.setText("Show system roles");
+                       boolean showSysRole = CurrentUser.isInRole(NodeConstants.ROLE_ADMIN);
+                       showSystemRoleBtn.setSelection(showSysRole);
+                       userFilter.setShowSystemRole(showSysRole);
+                       showSystemRoleBtn.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = -7033424592697691676L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       userFilter.setShowSystemRole(showSystemRoleBtn.getSelection());
+                                       refresh();
+                               }
+                       });
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       List<User> users = (List<User>) editor.getFlatGroups(null);
+                       List<User> filteredUsers = new ArrayList<User>();
+                       if (users.contains(user))
+                               users.remove(user);
+                       userFilter.setSearchText(filter);
+                       for (User user : users)
+                               if (userFilter.select(null, null, user))
+                                       filteredUsers.add(user);
+                       return filteredUsers;
+               }
+       }
+
+       private void addRemoveAbitily(SectionPart sectionPart, TableViewer userViewer, User user) {
+               Section section = sectionPart.getSection();
+               ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT);
+               ToolBar toolbar = toolBarManager.createControl(section);
+               final Cursor handCursor = new Cursor(section.getDisplay(), SWT.CURSOR_HAND);
+               toolbar.setCursor(handCursor);
+               toolbar.addDisposeListener(new DisposeListener() {
+                       private static final long serialVersionUID = 3882131405820522925L;
+
+                       public void widgetDisposed(DisposeEvent e) {
+                               if ((handCursor != null) && (handCursor.isDisposed() == false)) {
+                                       handCursor.dispose();
+                               }
+                       }
+               });
+
+               String tooltip = "Remove " + UserAdminUtils.getUserLocalId(user.getName()) + " from the below selected groups";
+               Action action = new RemoveMembershipAction(userViewer, user, tooltip, SecurityAdminImages.ICON_REMOVE_DESC);
+               toolBarManager.add(action);
+               toolBarManager.update(true);
+               section.setTextClient(toolbar);
+       }
+
+       private class RemoveMembershipAction extends Action {
+               private static final long serialVersionUID = -1337713097184522588L;
+
+               private final TableViewer userViewer;
+               private final User user;
+
+               RemoveMembershipAction(TableViewer userViewer, User user, String name, ImageDescriptor img) {
+                       super(name, img);
+                       this.userViewer = userViewer;
+                       this.user = user;
+               }
+
+               @Override
+               public void run() {
+                       ISelection selection = userViewer.getSelection();
+                       if (selection.isEmpty())
+                               return;
+
+                       @SuppressWarnings("unchecked")
+                       Iterator<Group> it = ((IStructuredSelection) selection).iterator();
+                       List<Group> groups = new ArrayList<Group>();
+                       while (it.hasNext()) {
+                               Group currGroup = it.next();
+                               groups.add(currGroup);
+                       }
+
+                       userAdminWrapper.beginTransactionIfNeeded();
+                       for (Group group : groups) {
+                               group.removeMember(user);
+                       }
+                       userAdminWrapper.commitOrNotifyTransactionStateChange();
+                       for (Group group : groups) {
+                               userAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+                       }
+               }
+       }
+
+       /**
+        * Defines the table as being a potential target to add group memberships
+        * (roles) to this user
+        */
+       private class GroupDropListener extends ViewerDropAdapter {
+               private static final long serialVersionUID = 2893468717831451621L;
+
+               private final UserAdminWrapper myUserAdminWrapper;
+               private final User myUser;
+
+               public GroupDropListener(UserAdminWrapper userAdminWrapper, Viewer userViewer, User user) {
+                       super(userViewer);
+                       this.myUserAdminWrapper = userAdminWrapper;
+                       this.myUser = user;
+               }
+
+               @Override
+               public boolean validateDrop(Object target, int operation, TransferData transferType) {
+                       // Target is always OK in a list only view
+                       // TODO check if not a string
+                       boolean validDrop = true;
+                       return validDrop;
+               }
+
+               @Override
+               public void drop(DropTargetEvent event) {
+                       String name = (String) event.data;
+                       UserAdmin myUserAdmin = myUserAdminWrapper.getUserAdmin();
+                       Role role = myUserAdmin.getRole(name);
+                       // TODO this check should be done before.
+                       if (role.getType() == Role.GROUP) {
+                               // TODO check if the user is already member of this group
+
+                               myUserAdminWrapper.beginTransactionIfNeeded();
+                               Group group = (Group) role;
+                               group.addMember(myUser);
+                               userAdminWrapper.commitOrNotifyTransactionStateChange();
+                               myUserAdminWrapper.notifyListeners(new UserAdminEvent(null, UserAdminEvent.ROLE_CHANGED, group));
+                       }
+                       super.drop(event);
+               }
+
+               @Override
+               public boolean performDrop(Object data) {
+                       // userTableViewerCmp.refresh();
+                       return true;
+               }
+       }
+
+       // LOCAL HELPERS
+       private void refreshFormTitle(User group) {
+               // getManagedForm().getForm().setText(UserAdminUtils.getProperty(group,
+               // LdapAttrs.cn.name()));
+       }
+
+       /** Appends a section with a title */
+       private Section addSection(FormToolkit tk, Composite parent, String title) {
+               Section section = tk.createSection(parent, Section.TITLE_BAR);
+               GridData gd = EclipseUiUtils.fillWidth();
+               gd.verticalAlignment = PRE_TITLE_INDENT;
+               section.setLayoutData(gd);
+               section.setText(title);
+               // section.getMenu().setVisible(true);
+
+               Composite body = tk.createComposite(section, SWT.WRAP);
+               body.setLayoutData(EclipseUiUtils.fillAll());
+               section.setClient(body);
+
+               return section;
+       }
+
+       /** Creates label and multiline text. */
+       Text createLMT(FormToolkit toolkit, Composite body, String label, String value) {
+               Label lbl = toolkit.createLabel(body, label);
+               lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               Text text = toolkit.createText(body, value, SWT.BORDER | SWT.MULTI);
+               text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true));
+               return text;
+       }
+
+       /** Creates label and password. */
+       Text createLP(FormToolkit toolkit, Composite body, String label, String value) {
+               Label lbl = toolkit.createLabel(body, label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               Text text = toolkit.createText(body, value, SWT.BORDER | SWT.PASSWORD);
+               text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               return text;
+       }
+
+       /** Creates label and text. */
+       Text createLT(FormToolkit toolkit, Composite parent, String label, String value) {
+               Label lbl = toolkit.createLabel(parent, label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+               Text text = toolkit.createText(parent, value, SWT.BORDER);
+               text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               CmsUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+               return text;
+       }
+
+       Text createReadOnlyLT(FormToolkit toolkit, Composite parent, String label, String value) {
+               Label lbl = toolkit.createLabel(parent, label);
+               lbl.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false));
+               lbl.setFont(EclipseUiUtils.getBoldFont(parent));
+               Text text = toolkit.createText(parent, value, SWT.NONE);
+               text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               text.setEditable(false);
+               CmsUtils.style(text, CmsWorkbenchStyles.WORKBENCH_FORM_TEXT);
+               return text;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UsersView.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/parts/UsersView.java
new file mode 100644 (file)
index 0000000..4a3b157
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.internal.useradmin.parts;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.useradmin.UiUserAdminListener;
+import org.argeo.cms.ui.workbench.internal.useradmin.UserAdminWrapper;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.CommonNameLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.DomainNameLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.MailLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserDragListener;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserNameLP;
+import org.argeo.cms.ui.workbench.internal.useradmin.providers.UserTableDefaultDClickListener;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.LdapObjs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.part.ViewPart;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/** List all users with filter - based on Ldif userAdmin */
+public class UsersView extends ViewPart implements ArgeoNames {
+       // private final static Log log = LogFactory.getLog(UsersView.class);
+
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".usersView";
+
+       /* DEPENDENCY INJECTION */
+       private UserAdminWrapper userAdminWrapper;
+
+       // UI Objects
+       private LdifUsersTable userTableViewerCmp;
+       private TableViewer userViewer;
+       private List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+
+       private UserAdminListener listener;
+
+       @Override
+       public void createPartControl(Composite parent) {
+
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+               // Define the displayed columns
+               columnDefs.add(new ColumnDefinition(new CommonNameLP(), "Common Name",
+                               150));
+               columnDefs.add(new ColumnDefinition(new MailLP(), "E-mail", 150));
+               columnDefs.add(new ColumnDefinition(new DomainNameLP(), "Domain", 200));
+               // Only show technical DN to admin
+               if (CurrentUser.isInRole(NodeConstants.ROLE_ADMIN))
+                       columnDefs.add(new ColumnDefinition(new UserNameLP(),
+                                       "Distinguished Name", 300));
+
+               // Create and configure the table
+               userTableViewerCmp = new MyUserTableViewer(parent, SWT.MULTI
+                               | SWT.H_SCROLL | SWT.V_SCROLL);
+               userTableViewerCmp.setLayoutData(EclipseUiUtils.fillAll());
+               userTableViewerCmp.setColumnDefinitions(columnDefs);
+               userTableViewerCmp.populate(true, false);
+
+               // Links
+               userViewer = userTableViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new UserTableDefaultDClickListener());
+               getViewSite().setSelectionProvider(userViewer);
+
+               // Really?
+               userTableViewerCmp.refresh();
+
+               // Drag and drop
+               int operations = DND.DROP_COPY | DND.DROP_MOVE;
+               Transfer[] tt = new Transfer[] { TextTransfer.getInstance() };
+               userViewer.addDragSupport(operations, tt, new UserDragListener(
+                               userViewer));
+
+               // Register a useradmin listener
+               listener = new MyUiUAListener(parent.getDisplay());
+               userAdminWrapper.addListener(listener);
+       }
+
+       private class MyUiUAListener extends UiUserAdminListener {
+               public MyUiUAListener(Display display) {
+                       super(display);
+               }
+
+               @Override
+               public void roleChangedToUiThread(UserAdminEvent event) {
+                       if (userViewer != null && !userViewer.getTable().isDisposed())
+                               refresh();
+               }
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private final String[] knownProps = { LdapAttrs.DN,
+                               LdapAttrs.uid.name(), LdapAttrs.cn.name(),
+                               LdapAttrs.givenName.name(), LdapAttrs.sn.name(),
+                               LdapAttrs.mail.name() };
+
+               public MyUserTableViewer(Composite parent, int style) {
+                       super(parent, style);
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       Role[] roles;
+
+                       try {
+                               StringBuilder builder = new StringBuilder();
+
+                               StringBuilder tmpBuilder = new StringBuilder();
+                               if (EclipseUiUtils.notEmpty(filter))
+                                       for (String prop : knownProps) {
+                                               tmpBuilder.append("(");
+                                               tmpBuilder.append(prop);
+                                               tmpBuilder.append("=*");
+                                               tmpBuilder.append(filter);
+                                               tmpBuilder.append("*)");
+                                       }
+                               if (tmpBuilder.length() > 1) {
+                                       builder.append("(&(").append(LdapAttrs.objectClass.name())
+                                                       .append("=").append(LdapObjs.inetOrgPerson.name())
+                                                       .append(")(|");
+                                       builder.append(tmpBuilder.toString());
+                                       builder.append("))");
+                               } else
+                                       builder.append("(").append(LdapAttrs.objectClass.name())
+                                                       .append("=").append(LdapObjs.inetOrgPerson.name())
+                                                       .append(")");
+                               roles = userAdminWrapper.getUserAdmin().getRoles(
+                                               builder.toString());
+                       } catch (InvalidSyntaxException e) {
+                               throw new CmsException("Unable to get roles with filter: "
+                                               + filter, e);
+                       }
+                       List<User> users = new ArrayList<User>();
+                       for (Role role : roles)
+                               // if (role.getType() == Role.USER && role.getType() !=
+                               // Role.GROUP)
+                               users.add((User) role);
+                       return users;
+               }
+       }
+
+       public void refresh() {
+               userTableViewerCmp.refresh();
+       }
+
+       // Override generic view methods
+       @Override
+       public void dispose() {
+               userAdminWrapper.removeListener(listener);
+               super.dispose();
+       }
+
+       @Override
+       public void setFocus() {
+               userTableViewerCmp.setFocus();
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserAdminWrapper(UserAdminWrapper userAdminWrapper) {
+               this.userAdminWrapper = userAdminWrapper;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/CommonNameLP.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/CommonNameLP.java
new file mode 100644 (file)
index 0000000..cbe6b6a
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.providers;
+
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.naming.LdapAttrs;
+import org.osgi.service.useradmin.User;
+
+/** Simply declare a label provider that returns the common name of a user */
+public class CommonNameLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 5256703081044911941L;
+
+       @Override
+       public String getText(User user) {
+               return UserAdminUtils.getProperty(user, LdapAttrs.cn.name());
+       }
+
+       @Override
+       public String getToolTipText(Object element) {
+               return UserAdminUtils.getProperty((User) element, LdapAttrs.DN);
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/DomainNameLP.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/DomainNameLP.java
new file mode 100644 (file)
index 0000000..eee16fb
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.providers;
+
+import org.argeo.cms.util.UserAdminUtils;
+import org.osgi.service.useradmin.User;
+
+/** The human friendly domain name for the corresponding user. */
+public class DomainNameLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 5256703081044911941L;
+
+       @Override
+       public String getText(User user) {
+               return UserAdminUtils.getDomainName(user);
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/MailLP.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/MailLP.java
new file mode 100644 (file)
index 0000000..3e4ff2c
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.providers;
+
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.naming.LdapAttrs;
+import org.osgi.service.useradmin.User;
+
+/** Simply declare a label provider that returns the Primary Mail of a user */
+public class MailLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 8329764452141982707L;
+
+       @Override
+       public String getText(User user) {
+               return UserAdminUtils.getProperty(user, LdapAttrs.mail.name());
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/RoleIconLP.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/RoleIconLP.java
new file mode 100644 (file)
index 0000000..d7e25c6
--- /dev/null
@@ -0,0 +1,35 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.providers;
+
+import org.argeo.cms.ui.workbench.internal.useradmin.SecurityAdminImages;
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeInstance;
+import org.eclipse.swt.graphics.Image;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/** Provide a bundle specific image depending on the current user type */
+public class RoleIconLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 6550449442061090388L;
+
+       @Override
+       public String getText(User user) {
+               return "";
+       }
+
+       @Override
+       public Image getImage(Object element) {
+               User user = (User) element;
+               String dn = user.getName();
+               if (dn.endsWith(NodeConstants.ROLES_BASEDN))
+                       return SecurityAdminImages.ICON_ROLE;
+               else if (user.getType() == Role.GROUP) {
+                       String businessCategory = UserAdminUtils.getProperty(user, LdapAttrs.businessCategory);
+                       if (businessCategory != null && businessCategory.equals(NodeInstance.WORKGROUP))
+                               return SecurityAdminImages.ICON_WORKGROUP;
+                       return SecurityAdminImages.ICON_GROUP;
+               } else
+                       return SecurityAdminImages.ICON_USER;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserAdminAbstractLP.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserAdminAbstractLP.java
new file mode 100644 (file)
index 0000000..45c0536
--- /dev/null
@@ -0,0 +1,66 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.providers;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.util.UserAdminUtils;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Utility class that add font modifications to a column label provider
+ * depending on the given user properties
+ */
+public abstract class UserAdminAbstractLP extends ColumnLabelProvider {
+       private static final long serialVersionUID = 137336765024922368L;
+
+       // private Font italic;
+       private Font bold;
+
+       @Override
+       public Font getFont(Object element) {
+               // Self as bold
+               try {
+                       LdapName selfUserName = UserAdminUtils.getCurrentUserLdapName();
+                       String userName = ((User) element).getName();
+                       LdapName userLdapName = new LdapName(userName);
+                       if (userLdapName.equals(selfUserName)) {
+                               if (bold == null)
+                                       bold = JFaceResources.getFontRegistry()
+                                                       .defaultFontDescriptor().setStyle(SWT.BOLD)
+                                                       .createFont(Display.getCurrent());
+                               return bold;
+                       }
+               } catch (InvalidNameException e) {
+                       throw new CmsException("cannot parse dn for " + element, e);
+               }
+
+               // Disabled as Italic
+               // Node userProfile = (Node) elem;
+               // if (!userProfile.getProperty(ARGEO_ENABLED).getBoolean())
+               // return italic;
+
+               return null;
+               // return super.getFont(element);
+       }
+
+       @Override
+       public String getText(Object element) {
+               User user = (User) element;
+               return getText(user);
+       }
+
+       public void setDisplay(Display display) {
+               // italic = JFaceResources.getFontRegistry().defaultFontDescriptor()
+               // .setStyle(SWT.ITALIC).createFont(display);
+               bold = JFaceResources.getFontRegistry().defaultFontDescriptor()
+                               .setStyle(SWT.BOLD).createFont(Display.getCurrent());
+       }
+
+       public abstract String getText(User user);
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserDragListener.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserDragListener.java
new file mode 100644 (file)
index 0000000..46b9d15
--- /dev/null
@@ -0,0 +1,40 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.providers;
+
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.dnd.DragSourceEvent;
+import org.eclipse.swt.dnd.DragSourceListener;
+import org.osgi.service.useradmin.User;
+
+/** Default drag listener to modify group and users via the UI */
+public class UserDragListener implements DragSourceListener {
+       private static final long serialVersionUID = -2074337775033781454L;
+       private final Viewer viewer;
+
+       public UserDragListener(Viewer viewer) {
+               this.viewer = viewer;
+       }
+
+       public void dragStart(DragSourceEvent event) {
+               // TODO implement finer checks
+               IStructuredSelection selection = (IStructuredSelection) viewer
+                               .getSelection();
+               if (selection.isEmpty() || selection.size() > 1)
+                       event.doit = false;
+               else
+                       event.doit = true;
+       }
+
+       public void dragSetData(DragSourceEvent event) {
+               // TODO Support multiple selection
+               Object obj = ((IStructuredSelection) viewer.getSelection())
+                               .getFirstElement();
+               if (obj != null) {
+                       User user = (User) obj;
+                       event.data = user.getName();
+               }
+       }
+
+       public void dragFinished(DragSourceEvent event) {
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserFilter.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserFilter.java
new file mode 100644 (file)
index 0000000..46bce8d
--- /dev/null
@@ -0,0 +1,58 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.providers;
+
+import static org.argeo.eclipse.ui.EclipseUiUtils.notEmpty;
+
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Filter user list using JFace mechanism on the client (yet on the server) side
+ * rather than having the UserAdmin to process the search
+ */
+public class UserFilter extends ViewerFilter {
+       private static final long serialVersionUID = 5082509381672880568L;
+
+       private String searchString;
+       private boolean showSystemRole = true;
+
+       private final String[] knownProps = { LdapAttrs.DN, LdapAttrs.cn.name(), LdapAttrs.givenName.name(),
+                       LdapAttrs.sn.name(), LdapAttrs.uid.name(), LdapAttrs.description.name(), LdapAttrs.mail.name() };
+
+       public void setSearchText(String s) {
+               // ensure that the value can be used for matching
+               if (notEmpty(s))
+                       searchString = ".*" + s.toLowerCase() + ".*";
+               else
+                       searchString = ".*";
+       }
+
+       public void setShowSystemRole(boolean showSystemRole) {
+               this.showSystemRole = showSystemRole;
+       }
+
+       @Override
+       public boolean select(Viewer viewer, Object parentElement, Object element) {
+               User user = (User) element;
+               if (!showSystemRole && user.getName().matches(".*(" + NodeConstants.ROLES_BASEDN + ")"))
+                       // UserAdminUtils.getProperty(user, LdifName.dn.name())
+                       // .toLowerCase().endsWith(AuthConstants.ROLES_BASEDN))
+                       return false;
+
+               if (searchString == null || searchString.length() == 0)
+                       return true;
+
+               if (user.getName().matches(searchString))
+                       return true;
+
+               for (String key : knownProps) {
+                       String currVal = UserAdminUtils.getProperty(user, key);
+                       if (notEmpty(currVal) && currVal.toLowerCase().matches(searchString))
+                               return true;
+               }
+               return false;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserNameLP.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserNameLP.java
new file mode 100644 (file)
index 0000000..0d8e850
--- /dev/null
@@ -0,0 +1,13 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.providers;
+
+import org.osgi.service.useradmin.User;
+
+/** Simply declare a label provider that returns the username of a user */
+public class UserNameLP extends UserAdminAbstractLP {
+       private static final long serialVersionUID = 6550449442061090388L;
+
+       @Override
+       public String getText(User user) {
+               return user.getName();
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserTableDefaultDClickListener.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserTableDefaultDClickListener.java
new file mode 100644 (file)
index 0000000..a25163b
--- /dev/null
@@ -0,0 +1,43 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.providers;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UserEditor;
+import org.argeo.cms.ui.workbench.internal.useradmin.parts.UserEditorInput;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Default double click listener for the various user tables, will open the
+ * clicked item in the editor
+ */
+public class UserTableDefaultDClickListener implements IDoubleClickListener {
+       public void doubleClick(DoubleClickEvent evt) {
+               if (evt.getSelection().isEmpty())
+                       return;
+               Object obj = ((IStructuredSelection) evt.getSelection())
+                               .getFirstElement();
+               User user = (User) obj;
+               IWorkbenchWindow iww = WorkbenchUiPlugin.getDefault().getWorkbench()
+                               .getActiveWorkbenchWindow();
+               IWorkbenchPage iwp = iww.getActivePage();
+               UserEditorInput uei = new UserEditorInput(user.getName());
+
+               try {
+                       // Works around the fact that dynamic setting of the editor icon
+                       // causes NPE after a login/logout on RAP
+                       if (user instanceof Group)
+                               iwp.openEditor(uei, UserEditor.GROUP_EDITOR_ID);
+                       else
+                               iwp.openEditor(uei, UserEditor.USER_EDITOR_ID);
+               } catch (PartInitException pie) {
+                       throw new CmsException("Unable to open UserEditor for " + user, pie);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserTransactionProvider.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/internal/useradmin/providers/UserTransactionProvider.java
new file mode 100644 (file)
index 0000000..a53cfb2
--- /dev/null
@@ -0,0 +1,74 @@
+package org.argeo.cms.ui.workbench.internal.useradmin.providers;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.transaction.Status;
+import javax.transaction.UserTransaction;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.eclipse.ui.AbstractSourceProvider;
+import org.eclipse.ui.ISources;
+
+/** Observe and notify UI on UserTransaction state changes */
+public class UserTransactionProvider extends AbstractSourceProvider {
+       private final static Log log = LogFactory
+                       .getLog(UserTransactionProvider.class);
+
+       public final static String TRANSACTION_STATE = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".userTransactionState";
+       public final static String STATUS_ACTIVE = "status.active";
+       public final static String STATUS_NO_TRANSACTION = "status.noTransaction";
+
+       /* DEPENDENCY INJECTION */
+       private UserTransaction userTransaction;
+
+       @Override
+       public String[] getProvidedSourceNames() {
+               return new String[] { TRANSACTION_STATE };
+       }
+
+       @Override
+       public Map<String, String> getCurrentState() {
+               Map<String, String> currentState = new HashMap<String, String>(1);
+               currentState.put(TRANSACTION_STATE, getInternalCurrentState());
+               return currentState;
+       }
+
+       @Override
+       public void dispose() {
+       }
+
+       private String getInternalCurrentState() {
+               try {
+                       String transactionState;
+                       if (userTransaction.getStatus() == Status.STATUS_NO_TRANSACTION)
+                               transactionState = STATUS_NO_TRANSACTION;
+                       else
+                               // if (userTransaction.getStatus() == Status.STATUS_ACTIVE)
+                               transactionState = STATUS_ACTIVE;
+                       return transactionState;
+               } catch (Exception e) {
+                       throw new CmsException("Unable to begin transaction", e);
+               }
+       }
+
+       /** Publishes the ability to notify a state change */
+       public void fireTransactionStateChange() {
+               try {
+                       fireSourceChanged(ISources.WORKBENCH, TRANSACTION_STATE,
+                                       getInternalCurrentState());
+               } catch (Exception e) {
+                       log.warn("Cannot fire transaction state change event. Caught exception: "
+                                       + e.getClass().getCanonicalName() + " - " + e.getMessage());
+               }
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setUserTransaction(UserTransaction userTransaction) {
+               this.userTransaction = userTransaction;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/DefaultNodeEditor.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/DefaultNodeEditor.java
new file mode 100644 (file)
index 0000000..e502dd4
--- /dev/null
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.security.AccessControlManager;
+import javax.jcr.security.Privilege;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.ChildNodesPage;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.GenericNodeEditorInput;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.GenericNodePage;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.GenericPropertyPage;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.NodePrivilegesPage;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.NodeVersionHistoryPage;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.forms.editor.FormEditor;
+
+/** Default form editor for a Jcr {@link Node} */
+public class DefaultNodeEditor extends FormEditor {
+       private static final long serialVersionUID = 8322127770921612239L;
+
+       // private final static Log log =
+       // LogFactory.getLog(GenericNodeEditor.class);
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".defaultNodeEditor";
+
+       private Node currentNode;
+
+       private GenericNodePage genericNodePage;
+       private GenericPropertyPage genericPropertyPage;
+       private ChildNodesPage childNodesPage;
+       private NodePrivilegesPage nodeRightsManagementPage;
+       private NodeVersionHistoryPage nodeVersionHistoryPage;
+
+       public void init(IEditorSite site, IEditorInput input)
+                       throws PartInitException {
+               super.init(site, input);
+               GenericNodeEditorInput nei = (GenericNodeEditorInput) getEditorInput();
+               currentNode = nei.getCurrentNode();
+               this.setPartName(JcrUtils.lastPathElement(nei.getPath()));
+       }
+
+       @Override
+       protected void addPages() {
+               try {
+                       genericPropertyPage = new GenericPropertyPage(this,
+                                       WorkbenchUiPlugin.getMessage("genericNodePageTitle"),
+                                       currentNode);
+                       addPage(genericPropertyPage);
+
+                       childNodesPage = new ChildNodesPage(this,
+                                       WorkbenchUiPlugin.getMessage("childNodesPageTitle"),
+                                       currentNode);
+                       addPage(childNodesPage);
+
+                       AccessControlManager accessControlManager = currentNode
+                                       .getSession().getAccessControlManager();
+                       List<Privilege> privileges = new ArrayList<Privilege>();
+                       privileges.add(accessControlManager
+                                       .privilegeFromName(Privilege.JCR_READ_ACCESS_CONTROL));
+                       if (accessControlManager.hasPrivileges(currentNode.getPath(),
+                                       privileges.toArray(new Privilege[0]))) {
+                               nodeRightsManagementPage = new NodePrivilegesPage(this,
+                                               WorkbenchUiPlugin
+                                                               .getMessage("nodeRightsManagementPageTitle"),
+                                               currentNode);
+                               addPage(nodeRightsManagementPage);
+                       }
+                       if (currentNode.isNodeType(NodeType.MIX_VERSIONABLE)) {
+                               nodeVersionHistoryPage = new NodeVersionHistoryPage(this,
+                                               WorkbenchUiPlugin
+                                                               .getMessage("nodeVersionHistoryPageTitle"),
+                                               currentNode);
+                               addPage(nodeVersionHistoryPage);
+                       }
+
+                       privileges = new ArrayList<Privilege>();
+                       privileges.add(accessControlManager
+                                       .privilegeFromName(Privilege.JCR_ALL));
+                       if (accessControlManager.hasPrivileges(currentNode.getPath(),
+                                       privileges.toArray(new Privilege[0]))) {
+                               genericNodePage = new GenericNodePage(
+                                               this,
+                                               WorkbenchUiPlugin.getMessage("propertyEditorPageTitle"),
+                                               currentNode);
+                               addPage(genericNodePage);
+                       }
+
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot get node info for "
+                                       + currentNode, e);
+               } catch (PartInitException e) {
+                       throw new EclipseUiException("Cannot add page "
+                                       + "on node editor for " + currentNode, e);
+               }
+       }
+
+       @Override
+       public void doSaveAs() {
+               // unused compulsory method
+       }
+
+       @Override
+       public void doSave(IProgressMonitor monitor) {
+               try {
+                       // Automatically commit all pages of the editor
+                       commitPages(true);
+                       firePropertyChange(PROP_DIRTY);
+               } catch (Exception e) {
+                       throw new EclipseUiException("Error while saving node", e);
+               }
+
+       }
+
+       @Override
+       public boolean isSaveAsAllowed() {
+               return true;
+       }
+
+       Node getCurrentNode() {
+               return currentNode;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/GenericJcrQueryEditor.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/GenericJcrQueryEditor.java
new file mode 100644 (file)
index 0000000..7c7f2b9
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.jcr;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.AbstractJcrQueryEditor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Text;
+
+/** Enables end user to type and execute any JCR query. */
+public class GenericJcrQueryEditor extends AbstractJcrQueryEditor {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID
+                       + ".genericJcrQueryEditor";
+
+       private Text queryField;
+
+       @Override
+       public void createQueryForm(Composite parent) {
+               parent.setLayout(new GridLayout(1, false));
+
+               queryField = new Text(parent, SWT.BORDER | SWT.MULTI | SWT.WRAP);
+               queryField.setText(initialQuery);
+               queryField.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               Button execute = new Button(parent, SWT.PUSH);
+               execute.setText("Execute");
+
+               Listener executeListener = new Listener() {
+                       private static final long serialVersionUID = -918256291554301699L;
+
+                       public void handleEvent(Event event) {
+                               executeQuery(queryField.getText());
+                       }
+               };
+
+               execute.addListener(SWT.Selection, executeListener);
+               // queryField.addListener(SWT.DefaultSelection, executeListener);
+       }
+
+       @Override
+       public void setFocus() {
+               queryField.setFocus();
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/JcrBrowserView.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/JcrBrowserView.java
new file mode 100644 (file)
index 0000000..f84950b
--- /dev/null
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.jcr;
+
+import java.util.List;
+
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.ObservationManager;
+
+import org.argeo.cms.ui.jcr.DefaultRepositoryRegister;
+import org.argeo.cms.ui.jcr.JcrBrowserUtils;
+import org.argeo.cms.ui.jcr.JcrDClickListener;
+import org.argeo.cms.ui.jcr.NodeContentProvider;
+import org.argeo.cms.ui.jcr.NodeLabelProvider;
+import org.argeo.cms.ui.jcr.PropertiesContentProvider;
+import org.argeo.cms.ui.jcr.RepositoryRegister;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.jcr.AsyncUiEventListener;
+import org.argeo.eclipse.ui.jcr.utils.NodeViewerComparer;
+import org.argeo.node.security.Keyring;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.ui.part.ViewPart;
+
+/**
+ * Basic View to display a sash form to browse a JCR compliant multiple
+ * repository environment
+ */
+public class JcrBrowserView extends ViewPart {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".jcrBrowserView";
+       private boolean sortChildNodes = true;
+
+       /* DEPENDENCY INJECTION */
+       private Keyring keyring;
+       private RepositoryRegister repositoryRegister = new DefaultRepositoryRegister();
+       private RepositoryFactory repositoryFactory;
+       private Repository nodeRepository;
+
+       // Current user session on the "Argeo node" default workspace
+       private Session userSession;
+
+       // This page widgets
+       private TreeViewer nodesViewer;
+       private NodeContentProvider nodeContentProvider;
+       private TableViewer propertiesViewer;
+       private EventListener resultsObserver;
+
+       @Override
+       public void createPartControl(Composite parent) {
+               parent.setLayout(new FillLayout());
+               SashForm sashForm = new SashForm(parent, SWT.VERTICAL);
+               sashForm.setSashWidth(4);
+               sashForm.setLayout(new FillLayout());
+
+               // Create the tree on top of the view
+               Composite top = new Composite(sashForm, SWT.NONE);
+               GridLayout gl = new GridLayout(1, false);
+               top.setLayout(gl);
+
+               try {
+                       this.userSession = this.nodeRepository.login();
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot open user session", e);
+               }
+
+               nodeContentProvider = new NodeContentProvider(userSession, keyring, repositoryRegister, repositoryFactory,
+                               sortChildNodes);
+
+               // nodes viewer
+               nodesViewer = createNodeViewer(top, nodeContentProvider);
+
+               // context menu : it is completely defined in the plugin.xml file.
+               MenuManager menuManager = new MenuManager();
+               Menu menu = menuManager.createContextMenu(nodesViewer.getTree());
+
+               nodesViewer.getTree().setMenu(menu);
+               getSite().registerContextMenu(menuManager, nodesViewer);
+               getSite().setSelectionProvider(nodesViewer);
+
+               nodesViewer.setInput(getViewSite());
+
+               // Create the property viewer on the bottom
+               Composite bottom = new Composite(sashForm, SWT.NONE);
+               bottom.setLayout(new GridLayout(1, false));
+               propertiesViewer = createPropertiesViewer(bottom);
+
+               sashForm.setWeights(getWeights());
+               nodesViewer.setComparer(new NodeViewerComparer());
+       }
+
+       public void refresh(Object obj) {
+               // Enable full refresh from a command when no element of the tree is
+               // selected
+               if (obj == null) {
+                       Object[] elements = nodeContentProvider.getElements(null);
+                       for (Object el : elements) {
+                               if (el instanceof TreeParent)
+                                       JcrBrowserUtils.forceRefreshIfNeeded((TreeParent) el);
+                               getNodeViewer().refresh(el);
+                       }
+               } else
+                       getNodeViewer().refresh(obj);
+       }
+
+       /**
+        * To be overridden to adapt size of form and result frames.
+        */
+       protected int[] getWeights() {
+               return new int[] { 70, 30 };
+       }
+
+       protected TreeViewer createNodeViewer(Composite parent, final ITreeContentProvider nodeContentProvider) {
+
+               final TreeViewer tmpNodeViewer = new TreeViewer(parent, SWT.MULTI);
+
+               tmpNodeViewer.getTree().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               tmpNodeViewer.setContentProvider(nodeContentProvider);
+               tmpNodeViewer.setLabelProvider(new NodeLabelProvider());
+               tmpNodeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+                       public void selectionChanged(SelectionChangedEvent event) {
+                               if (!event.getSelection().isEmpty()) {
+                                       IStructuredSelection sel = (IStructuredSelection) event.getSelection();
+                                       Object firstItem = sel.getFirstElement();
+                                       if (firstItem instanceof SingleJcrNodeElem)
+                                               propertiesViewer.setInput(((SingleJcrNodeElem) firstItem).getNode());
+                               } else {
+                                       propertiesViewer.setInput(getViewSite());
+                               }
+                       }
+               });
+
+               resultsObserver = new TreeObserver(tmpNodeViewer.getTree().getDisplay());
+               if (keyring != null)
+                       try {
+                               ObservationManager observationManager = userSession.getWorkspace().getObservationManager();
+                               observationManager.addEventListener(resultsObserver, Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED, "/",
+                                               true, null, null, false);
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Cannot register listeners", e);
+                       }
+
+               tmpNodeViewer.addDoubleClickListener(new JcrDClickListener(tmpNodeViewer));
+               return tmpNodeViewer;
+       }
+
+       protected TableViewer createPropertiesViewer(Composite parent) {
+               propertiesViewer = new TableViewer(parent);
+               propertiesViewer.getTable().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               propertiesViewer.getTable().setHeaderVisible(true);
+               propertiesViewer.setContentProvider(new PropertiesContentProvider());
+               TableViewerColumn col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Name");
+               col.getColumn().setWidth(200);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -6684361063107478595L;
+
+                       public String getText(Object element) {
+                               try {
+                                       return ((Property) element).getName();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unexpected exception in label provider", e);
+                               }
+                       }
+               });
+               col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Value");
+               col.getColumn().setWidth(400);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -8201994187693336657L;
+
+                       public String getText(Object element) {
+                               try {
+                                       Property property = (Property) element;
+                                       if (property.getType() == PropertyType.BINARY)
+                                               return "<binary>";
+                                       else if (property.isMultiple()) {
+                                               StringBuffer buf = new StringBuffer("[");
+                                               Value[] values = property.getValues();
+                                               for (int i = 0; i < values.length; i++) {
+                                                       if (i != 0)
+                                                               buf.append(", ");
+                                                       buf.append(values[i].getString());
+                                               }
+                                               buf.append(']');
+                                               return buf.toString();
+                                       } else
+                                               return property.getValue().getString();
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Unexpected exception in label provider", e);
+                               }
+                       }
+               });
+               col = new TableViewerColumn(propertiesViewer, SWT.NONE);
+               col.getColumn().setText("Type");
+               col.getColumn().setWidth(200);
+               col.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -6009599998150286070L;
+
+                       public String getText(Object element) {
+                               return JcrBrowserUtils.getPropertyTypeAsString((Property) element);
+                       }
+               });
+               propertiesViewer.setInput(getViewSite());
+               return propertiesViewer;
+       }
+
+       @Override
+       public void dispose() {
+               super.dispose();
+       }
+
+       protected TreeViewer getNodeViewer() {
+               return nodesViewer;
+       }
+
+       /**
+        * Resets the tree content provider
+        * 
+        * @param sortChildNodes
+        *            if true the content provider will use a comparer to sort nodes
+        *            that might slow down the display
+        */
+       public void setSortChildNodes(boolean sortChildNodes) {
+               this.sortChildNodes = sortChildNodes;
+               ((NodeContentProvider) nodesViewer.getContentProvider()).setSortChildren(sortChildNodes);
+               nodesViewer.setInput(getViewSite());
+       }
+
+       /** Notifies the current view that a node has been added */
+       public void nodeAdded(TreeParent parentNode) {
+               // insure that Ui objects have been correctly created:
+               JcrBrowserUtils.forceRefreshIfNeeded(parentNode);
+               getNodeViewer().refresh(parentNode);
+               getNodeViewer().expandToLevel(parentNode, 1);
+       }
+
+       /** Notifies the current view that a node has been removed */
+       public void nodeRemoved(TreeParent parentNode) {
+               IStructuredSelection newSel = new StructuredSelection(parentNode);
+               getNodeViewer().setSelection(newSel, true);
+               // Force refresh
+               IStructuredSelection tmpSel = (IStructuredSelection) getNodeViewer().getSelection();
+               getNodeViewer().refresh(tmpSel.getFirstElement());
+       }
+
+       class TreeObserver extends AsyncUiEventListener {
+
+               public TreeObserver(Display display) {
+                       super(display);
+               }
+
+               @Override
+               protected Boolean willProcessInUiThread(List<Event> events) throws RepositoryException {
+                       for (Event event : events) {
+                               if (getLog().isTraceEnabled())
+                                       getLog().debug("Received event " + event);
+                               String path = event.getPath();
+                               int index = path.lastIndexOf('/');
+                               String propertyName = path.substring(index + 1);
+                               if (getLog().isTraceEnabled())
+                                       getLog().debug("Concerned property " + propertyName);
+                       }
+                       return false;
+               }
+
+               protected void onEventInUiThread(List<Event> events) throws RepositoryException {
+                       if (getLog().isTraceEnabled())
+                               getLog().trace("Refresh result list");
+                       nodesViewer.refresh();
+               }
+
+       }
+
+       public boolean getSortChildNodes() {
+               return sortChildNodes;
+       }
+
+       @Override
+       public void setFocus() {
+               getNodeViewer().getTree().setFocus();
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setRepositoryRegister(RepositoryRegister repositoryRegister) {
+               this.repositoryRegister = repositoryRegister;
+       }
+
+       public void setKeyring(Keyring keyring) {
+               this.keyring = keyring;
+       }
+
+       public void setRepositoryFactory(RepositoryFactory repositoryFactory) {
+               this.repositoryFactory = repositoryFactory;
+       }
+
+       public void setNodeRepository(Repository nodeRepository) {
+               this.nodeRepository = nodeRepository;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/NodeFsBrowserView.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/NodeFsBrowserView.java
new file mode 100644 (file)
index 0000000..cba228f
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.jcr;
+
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.spi.FileSystemProvider;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.fs.SimpleFsBrowser;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.part.ViewPart;
+
+/** Browse the node file system. */
+public class NodeFsBrowserView extends ViewPart {
+       public final static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".nodeFsBrowserView";
+
+       private FileSystemProvider nodeFileSystemProvider;
+
+       @Override
+       public void createPartControl(Composite parent) {
+               try {
+                       URI uri = new URI("node:///");
+                       FileSystem fileSystem = nodeFileSystemProvider.getFileSystem(uri);
+                       if (fileSystem == null)
+                               fileSystem = nodeFileSystemProvider.newFileSystem(uri, null);
+                       Path nodePath = fileSystem.getPath("~");
+                       SimpleFsBrowser browser = new SimpleFsBrowser(parent, SWT.NO_FOCUS);
+                       browser.setInput(nodePath);
+               } catch (Exception e) {
+                       throw new CmsException("Cannot open file system browser", e);
+               }
+       }
+
+       @Override
+       public void setFocus() {
+       }
+
+       public void setNodeFileSystemProvider(FileSystemProvider nodeFileSystemProvider) {
+               this.nodeFileSystemProvider = nodeFileSystemProvider;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/WorkbenchJcrDClickListener.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/jcr/WorkbenchJcrDClickListener.java
new file mode 100644 (file)
index 0000000..37feeb7
--- /dev/null
@@ -0,0 +1,101 @@
+package org.argeo.cms.ui.workbench.jcr;
+
+import static javax.jcr.Node.JCR_CONTENT;
+import static javax.jcr.Property.JCR_DATA;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.ui.jcr.JcrDClickListener;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.cms.ui.workbench.internal.jcr.parts.GenericNodeEditorInput;
+import org.argeo.cms.ui.workbench.util.CommandUtils;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.specific.OpenFile;
+import org.argeo.eclipse.ui.specific.SingleSourcingException;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.jface.viewers.TreeViewer;
+
+public class WorkbenchJcrDClickListener extends JcrDClickListener {
+
+       public WorkbenchJcrDClickListener(TreeViewer nodeViewer) {
+               super(nodeViewer);
+       }
+
+       @Override
+       protected void openNode(Node node) {
+               try {
+                       if (node.isNodeType(NodeType.NT_FILE)) {
+                               // Also open it
+
+                               String name = node.getName();
+                               Map<String, String> params = new HashMap<String, String>();
+                               params.put(OpenFile.PARAM_FILE_NAME, name);
+
+                               // TODO rather directly transmit the path to the node, once
+                               // we have defined convention to provide an Absolute URI to
+                               // a node in a multi repo / workspace / user context
+                               // params.put(OpenFile.PARAM_FILE_URI,
+                               // OpenFileService.JCR_SCHEME + node.getPath());
+
+                               // we copy the node to a tmp file to be opened as a dirty
+                               // workaround
+                               File tmpFile = null;
+                               // OutputStream os = null;
+                               // InputStream is = null;
+                               int i = name.lastIndexOf('.');
+                               String prefix, suffix;
+                               if (i == -1) {
+                                       prefix = name;
+                                       suffix = null;
+                               } else {
+                                       prefix = name.substring(0, i);
+                                       suffix = name.substring(i);
+                               }
+                               Binary binary = null;
+                               try {
+                                       tmpFile = File.createTempFile(prefix, suffix);
+                                       tmpFile.deleteOnExit();
+                               } catch (IOException e1) {
+                                       throw new EclipseUiException("Cannot create temp file", e1);
+                               }
+                               try (OutputStream os = new FileOutputStream(tmpFile)) {
+                                       binary = node.getNode(JCR_CONTENT).getProperty(JCR_DATA).getBinary();
+                                       try (InputStream is = binary.getStream();) {
+                                               IOUtils.copy(is, os);
+                                       }
+                               } catch (IOException e) {
+                                       throw new SingleSourcingException("Cannot open file " + prefix + "." + suffix, e);
+                               } finally {
+                                       // IOUtils.closeQuietly(is);
+                                       // IOUtils.closeQuietly(os);
+                                       JcrUtils.closeQuietly(binary);
+                               }
+                               Path path = Paths.get(tmpFile.getAbsolutePath());
+                               String uri = path.toUri().toString();
+                               params.put(OpenFile.PARAM_FILE_URI, uri);
+                               CommandUtils.callCommand(OpenFile.ID, params);
+                       }
+                       GenericNodeEditorInput gnei = new GenericNodeEditorInput(node);
+                       WorkbenchUiPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow().getActivePage().openEditor(gnei,
+                                       DefaultNodeEditor.ID);
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Repository error while getting node info", re);
+               } catch (Exception pie) {
+                       throw new EclipseUiException("Unexpected exception while opening node editor", pie);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/messages.properties b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/messages.properties
new file mode 100644 (file)
index 0000000..9994a5a
--- /dev/null
@@ -0,0 +1,29 @@
+## English labels for Agreo JCR UI application 
+
+## Generic labels 
+
+## Errors & warnings 
+errorUnvalidNtFolderNodeType= Error: folder can only be created on a Jcr Node
+warningInvalidNodeToImport=Can only import to a node
+warningInvalidMultipleSelection=This functionality is implemented only on a single node for the time being.
+warningUnversionableNode= Current node is not versionable.
+warningNoChildNode= Current node has no child.
+
+## Commands 
+getNodeSizeCmdLbl= Get approx. size
+addFolderNodeCmdLbl= Add Folder
+
+## GenericNodeEditor 
+nodeEditorLbl=Generic node editor
+genericNodePageTitle=Properties
+childNodesPageTitle=Children
+nodeRightsManagementPageTitle=Effective privileges
+nodeVersionHistoryPageTitle=History
+propertyEditorPageTitle=Properties Editor (Experimental)
+
+# History 
+versionTreeSectionTitle=Version list
+versionHistorySectionTitle=History
+## Dummy ones 
+testLbl=Internationalizations of messages seems to work properly.
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/BundleNode.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/BundleNode.java
new file mode 100644 (file)
index 0000000..6dd9ac4
--- /dev/null
@@ -0,0 +1,46 @@
+package org.argeo.cms.ui.workbench.osgi;
+
+import org.argeo.eclipse.ui.TreeParent;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+
+/** A tree element representing a {@link Bundle} */
+class BundleNode extends TreeParent {
+       private final Bundle bundle;
+
+       public BundleNode(Bundle bundle) {
+               this(bundle, false);
+       }
+
+       @SuppressWarnings("rawtypes")
+       public BundleNode(Bundle bundle, boolean hasChildren) {
+               super(bundle.getSymbolicName());
+               this.bundle = bundle;
+
+               if (hasChildren) {
+                       // REFERENCES
+                       ServiceReference[] usedServices = bundle.getServicesInUse();
+                       if (usedServices != null) {
+                               for (ServiceReference sr : usedServices) {
+                                       if (sr != null)
+                                               addChild(new ServiceReferenceNode(sr, false));
+                               }
+                       }
+
+                       // SERVICES
+                       ServiceReference[] registeredServices = bundle
+                                       .getRegisteredServices();
+                       if (registeredServices != null) {
+                               for (ServiceReference sr : registeredServices) {
+                                       if (sr != null)
+                                               addChild(new ServiceReferenceNode(sr, true));
+                               }
+                       }
+               }
+
+       }
+
+       Bundle getBundle() {
+               return bundle;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/BundlesView.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/BundlesView.java
new file mode 100644 (file)
index 0000000..4032e78
--- /dev/null
@@ -0,0 +1,126 @@
+//package org.argeo.eclipse.ui.workbench.osgi;
+//public class BundlesView {}
+
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.osgi;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.ColumnViewerComparator;
+import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.part.ViewPart;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+/**
+ * Overview of the bundles as a table. Equivalent to Equinox 'ss' console
+ * command.
+ */
+public class BundlesView extends ViewPart {
+       private TableViewer viewer;
+
+       @Override
+       public void createPartControl(Composite parent) {
+               viewer = new TableViewer(parent);
+               viewer.setContentProvider(new BundleContentProvider());
+               viewer.getTable().setHeaderVisible(true);
+
+               EclipseUiSpecificUtils.enableToolTipSupport(viewer);
+
+               // ID
+               TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(30);
+               column.getColumn().setText("ID");
+               column.getColumn().setAlignment(SWT.RIGHT);
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -3122136344359358605L;
+
+                       public String getText(Object element) {
+                               return Long.toString(((Bundle) element).getBundleId());
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // State
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(18);
+               column.getColumn().setText("State");
+               column.setLabelProvider(new StateLabelProvider());
+               new ColumnViewerComparator(column);
+
+               // Symbolic name
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(250);
+               column.getColumn().setText("Symbolic Name");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -4280840684440451080L;
+
+                       public String getText(Object element) {
+                               return ((Bundle) element).getSymbolicName();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Version
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(150);
+               column.getColumn().setText("Version");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = 6871926308708629989L;
+
+                       public String getText(Object element) {
+                               Bundle bundle = (org.osgi.framework.Bundle) element;
+                               return bundle.getVersion().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               viewer.setInput(WorkbenchUiPlugin.getDefault().getBundle().getBundleContext());
+
+       }
+
+       @Override
+       public void setFocus() {
+               if (viewer != null)
+                       viewer.getControl().setFocus();
+       }
+
+       /** Content provider managing the array of bundles */
+       private static class BundleContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = -8533792785725875977L;
+
+               public Object[] getElements(Object inputElement) {
+                       if (inputElement instanceof BundleContext) {
+                               BundleContext bc = (BundleContext) inputElement;
+                               return bc.getBundles();
+                       }
+                       return null;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/CmsSessionsView.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/CmsSessionsView.java
new file mode 100644 (file)
index 0000000..44b554e
--- /dev/null
@@ -0,0 +1,186 @@
+//package org.argeo.eclipse.ui.workbench.osgi;
+//public class BundlesView {}
+
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.osgi;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import javax.naming.ldap.LdapName;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CmsSession;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.ColumnViewerComparator;
+import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils;
+import org.argeo.util.LangUtils;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.part.ViewPart;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+/**
+ * Overview of the active CMS sessions.
+ */
+public class CmsSessionsView extends ViewPart {
+       private TableViewer viewer;
+
+       @Override
+       public void createPartControl(Composite parent) {
+               viewer = new TableViewer(parent);
+               viewer.setContentProvider(new CmsSessionContentProvider());
+               viewer.getTable().setHeaderVisible(true);
+
+               EclipseUiSpecificUtils.enableToolTipSupport(viewer);
+
+               int longColWidth = 150;
+               int smallColWidth = 100;
+
+               // Display name
+               TableViewerColumn column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(longColWidth);
+               column.getColumn().setText("User");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return ((CmsSession) element).getAuthorization().toString();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return ((CmsSession) element).getUserDn().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Creation time
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("Since");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return LangUtils.since(((CmsSession) element).getCreationTime());
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return ((CmsSession) element).getCreationTime().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Username
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("Username");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               LdapName userDn = ((CmsSession) element).getUserDn();
+                               return userDn.getRdn(userDn.size() - 1).getValue().toString();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return ((CmsSession) element).getUserDn().toString();
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // UUID
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("UUID");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return ((CmsSession) element).getUuid().toString();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return getText(element);
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               // Local ID
+               column = new TableViewerColumn(viewer, SWT.NONE);
+               column.getColumn().setWidth(smallColWidth);
+               column.getColumn().setText("Local ID");
+               column.setLabelProvider(new ColumnLabelProvider() {
+                       private static final long serialVersionUID = -5234573509093747505L;
+
+                       public String getText(Object element) {
+                               return ((CmsSession) element).getLocalId();
+                       }
+
+                       public String getToolTipText(Object element) {
+                               return getText(element);
+                       }
+               });
+               new ColumnViewerComparator(column);
+
+               viewer.setInput(WorkbenchUiPlugin.getDefault().getBundle().getBundleContext());
+
+       }
+
+       @Override
+       public void setFocus() {
+               if (viewer != null)
+                       viewer.getControl().setFocus();
+       }
+
+       /** Content provider managing the array of bundles */
+       private static class CmsSessionContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = -8533792785725875977L;
+
+               public Object[] getElements(Object inputElement) {
+                       if (inputElement instanceof BundleContext) {
+                               BundleContext bc = (BundleContext) inputElement;
+                               Collection<ServiceReference<CmsSession>> srs;
+                               try {
+                                       srs = bc.getServiceReferences(CmsSession.class, null);
+                               } catch (InvalidSyntaxException e) {
+                                       throw new CmsException("Cannot retrieve CMS sessions", e);
+                               }
+                               List<CmsSession> res = new ArrayList<>();
+                               for (ServiceReference<CmsSession> sr : srs) {
+                                       res.add(bc.getService(sr));
+                               }
+                               return res.toArray();
+                       }
+                       return null;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/ModulesView.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/ModulesView.java
new file mode 100644 (file)
index 0000000..235261a
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.osgi;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.TreeParent;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.part.ViewPart;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+/** The OSGi runtime from a module perspective. */
+public class ModulesView extends ViewPart {
+       private TreeViewer viewer;
+
+       @Override
+       public void createPartControl(Composite parent) {
+               viewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+               viewer.setContentProvider(new ModulesContentProvider());
+               viewer.setLabelProvider(new ModulesLabelProvider());
+               viewer.setInput(WorkbenchUiPlugin.getDefault().getBundle()
+                               .getBundleContext());
+       }
+
+       @Override
+       public void setFocus() {
+               viewer.getTree().setFocus();
+       }
+
+       private class ModulesContentProvider implements ITreeContentProvider {
+               private static final long serialVersionUID = 3819934804640641721L;
+
+               public Object[] getElements(Object inputElement) {
+                       return getChildren(inputElement);
+               }
+
+               public Object[] getChildren(Object parentElement) {
+                       if (parentElement instanceof BundleContext) {
+                               BundleContext bundleContext = (BundleContext) parentElement;
+                               Bundle[] bundles = bundleContext.getBundles();
+
+                               List<BundleNode> modules = new ArrayList<BundleNode>();
+                               for (Bundle bundle : bundles) {
+                                       if (bundle.getState() == Bundle.ACTIVE)
+                                               modules.add(new BundleNode(bundle, true));
+                               }
+                               return modules.toArray();
+                       } else if (parentElement instanceof TreeParent) {
+                               return ((TreeParent) parentElement).getChildren();
+                       } else {
+                               return null;
+                       }
+               }
+
+               public Object getParent(Object element) {
+                       // TODO Auto-generated method stub
+                       return null;
+               }
+
+               public boolean hasChildren(Object element) {
+                       if (element instanceof TreeParent) {
+                               return ((TreeParent) element).hasChildren();
+                       }
+                       return false;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+
+       private class ModulesLabelProvider extends StateLabelProvider {
+               private static final long serialVersionUID = 5290046145534824722L;
+
+               @Override
+               public String getText(Object element) {
+                       if (element instanceof BundleNode)
+                               return element.toString() + " ["
+                                               + ((BundleNode) element).getBundle().getBundleId()
+                                               + "]";
+                       return element.toString();
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/MultiplePackagesView.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/MultiplePackagesView.java
new file mode 100644 (file)
index 0000000..a7f9a53
--- /dev/null
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.osgi;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.TreeParent;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.part.ViewPart;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.packageadmin.ExportedPackage;
+import org.osgi.service.packageadmin.PackageAdmin;
+
+/** <b>Experimental</b> The OSGi runtime from a module perspective. */
+@SuppressWarnings({ "deprecation", "rawtypes", "unchecked" })
+public class MultiplePackagesView extends ViewPart {
+       private TreeViewer viewer;
+       private PackageAdmin packageAdmin;
+       private Comparator<ExportedPackage> epc = new Comparator<ExportedPackage>() {
+               public int compare(ExportedPackage o1, ExportedPackage o2) {
+                       if (!o1.getName().equals(o2.getName()))
+                               return o1.getName().compareTo(o2.getName());
+                       else
+                               return o1.getVersion().compareTo(o2.getVersion());
+               }
+       };
+
+       @Override
+       public void createPartControl(Composite parent) {
+               viewer = new TreeViewer(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL);
+               viewer.setContentProvider(new ModulesContentProvider());
+               viewer.setLabelProvider(new LabelProvider());
+               viewer.setInput(WorkbenchUiPlugin.getDefault().getBundle()
+                               .getBundleContext());
+       }
+
+       @Override
+       public void setFocus() {
+               viewer.getTree().setFocus();
+       }
+
+       private class ModulesContentProvider implements ITreeContentProvider {
+               private static final long serialVersionUID = 3819934804640641721L;
+
+               public Object[] getElements(Object inputElement) {
+                       return getChildren(inputElement);
+               }
+
+               public Object[] getChildren(Object parentElement) {
+                       if (parentElement instanceof BundleContext) {
+                               BundleContext bundleContext = (BundleContext) parentElement;
+                               Bundle[] bundles = bundleContext.getBundles();
+
+                               // scan packages
+                               ServiceReference paSr = bundleContext
+                                               .getServiceReference(PackageAdmin.class.getName());
+                               // TODO: make a cleaner referencing
+                               packageAdmin = (PackageAdmin) bundleContext.getService(paSr);
+
+                               Map<Bundle, Set<ExportedPackage>> imported = new HashMap<Bundle, Set<ExportedPackage>>();
+                               Map<String, Set<ExportedPackage>> packages = new TreeMap<String, Set<ExportedPackage>>();
+                               for (Bundle bundle : bundles) {
+                                       processBundle(bundle, imported, packages);
+                               }
+
+                               List<MultiplePackagesNode> multiplePackages = new ArrayList<MultiplePackagesNode>();
+                               for (String packageName : packages.keySet()) {
+                                       Set<ExportedPackage> pkgs = packages.get(packageName);
+                                       if (pkgs.size() > 1) {
+                                               MultiplePackagesNode mpn = new MultiplePackagesNode(
+                                                               packageName, pkgs);
+                                               multiplePackages.add(mpn);
+                                       }
+                               }
+
+                               return multiplePackages.toArray();
+                       } else if (parentElement instanceof TreeParent) {
+                               return ((TreeParent) parentElement).getChildren();
+                       } else {
+                               return null;
+                       }
+               }
+
+               protected void processBundle(Bundle bundle,
+                               Map<Bundle, Set<ExportedPackage>> imported,
+                               Map<String, Set<ExportedPackage>> packages) {
+                       ExportedPackage[] pkgs = packageAdmin.getExportedPackages(bundle);
+                       if (pkgs == null)
+                               return;
+                       for (ExportedPackage pkg : pkgs) {
+                               if (!packages.containsKey(pkg.getName()))
+                                       packages.put(pkg.getName(), new TreeSet<ExportedPackage>(
+                                                       epc));
+                               Set<ExportedPackage> expPackages = packages.get(pkg.getName());
+                               expPackages.add(pkg);
+
+                               // imported
+                               for (Bundle b : pkg.getImportingBundles()) {
+                                       if (bundle.getBundleId() != b.getBundleId()) {
+                                               if (!imported.containsKey(b)) {
+                                                       imported.put(b, new TreeSet<ExportedPackage>(epc));
+                                               }
+                                               Set<ExportedPackage> impPackages = imported.get(b);
+                                               impPackages.add(pkg);
+                                       }
+                               }
+                       }
+               }
+
+               public Object getParent(Object element) {
+                       // TODO Auto-generated method stub
+                       return null;
+               }
+
+               public boolean hasChildren(Object element) {
+                       if (element instanceof TreeParent) {
+                               return ((TreeParent) element).hasChildren();
+                       }
+                       return false;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+
+       private class MultiplePackagesNode extends TreeParent {
+               public MultiplePackagesNode(String packageName,
+                               Set<ExportedPackage> exportedPackages) {
+                       super(packageName);
+                       for (ExportedPackage pkg : exportedPackages) {
+                               addChild(new ExportedPackageNode(pkg));
+                       }
+               }
+       }
+
+       private class ExportedPackageNode extends TreeParent {
+               public ExportedPackageNode(ExportedPackage exportedPackage) {
+                       super(exportedPackage.getName() + " - "
+                                       + exportedPackage.getVersion() + " ("
+                                       + exportedPackage.getExportingBundle() + ")");
+                       for (Bundle bundle : exportedPackage.getImportingBundles()) {
+                               addChild(new BundleNode(bundle, true));
+                       }
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/OsgiExplorerImages.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/OsgiExplorerImages.java
new file mode 100644 (file)
index 0000000..1233e11
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.osgi;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.eclipse.swt.graphics.Image;
+
+/** Shared icons. */
+public class OsgiExplorerImages {
+       public final static Image INSTALLED = WorkbenchUiPlugin.getImageDescriptor(
+                       "icons/installed.gif").createImage();
+       public final static Image RESOLVED = WorkbenchUiPlugin.getImageDescriptor(
+                       "icons/resolved.gif").createImage();
+       public final static Image STARTING = WorkbenchUiPlugin.getImageDescriptor(
+                       "icons/starting.gif").createImage();
+       public final static Image ACTIVE = WorkbenchUiPlugin.getImageDescriptor(
+                       "icons/active.gif").createImage();
+       public final static Image SERVICE_PUBLISHED = WorkbenchUiPlugin
+                       .getImageDescriptor("icons/service_published.gif").createImage();
+       public final static Image SERVICE_REFERENCED = WorkbenchUiPlugin
+                       .getImageDescriptor("icons/service_referenced.gif").createImage();
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/ServiceReferenceNode.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/ServiceReferenceNode.java
new file mode 100644 (file)
index 0000000..6b4972d
--- /dev/null
@@ -0,0 +1,46 @@
+package org.argeo.cms.ui.workbench.osgi;
+
+import org.argeo.eclipse.ui.TreeParent;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+
+/** A tree element representing a {@link ServiceReference} */
+@SuppressWarnings({ "rawtypes" })
+class ServiceReferenceNode extends TreeParent {
+       private final ServiceReference serviceReference;
+       private final boolean published;
+
+       public ServiceReferenceNode(ServiceReference serviceReference,
+                       boolean published) {
+               super(serviceReference.toString());
+               this.serviceReference = serviceReference;
+               this.published = published;
+
+               if (isPublished()) {
+                       Bundle[] usedBundles = serviceReference.getUsingBundles();
+                       if (usedBundles != null) {
+                               for (Bundle b : usedBundles) {
+                                       if (b != null)
+                                               addChild(new BundleNode(b));
+                               }
+                       }
+               } else {
+                       Bundle provider = serviceReference.getBundle();
+                       addChild(new BundleNode(provider));
+               }
+
+               for (String key : serviceReference.getPropertyKeys()) {
+                       addChild(new TreeParent(key + "="
+                                       + serviceReference.getProperty(key)));
+               }
+
+       }
+
+       public ServiceReference getServiceReference() {
+               return serviceReference;
+       }
+
+       public boolean isPublished() {
+               return published;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/StateLabelProvider.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/osgi/StateLabelProvider.java
new file mode 100644 (file)
index 0000000..86b67c3
--- /dev/null
@@ -0,0 +1,82 @@
+package org.argeo.cms.ui.workbench.osgi;
+
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+
+/** Label provider showing the sate of bundles */
+class StateLabelProvider extends ColumnLabelProvider {
+       private static final long serialVersionUID = -7885583135316000733L;
+
+       @Override
+       public Image getImage(Object element) {
+               int state;
+               if (element instanceof Bundle)
+                       state = ((Bundle) element).getState();
+               else if (element instanceof BundleNode)
+                       state = ((BundleNode) element).getBundle().getState();
+               else if (element instanceof ServiceReferenceNode)
+                       if (((ServiceReferenceNode) element).isPublished())
+                               return OsgiExplorerImages.SERVICE_PUBLISHED;
+                       else
+                               return OsgiExplorerImages.SERVICE_REFERENCED;
+               else
+                       return null;
+
+               switch (state) {
+               case Bundle.UNINSTALLED:
+                       return OsgiExplorerImages.INSTALLED;
+               case Bundle.INSTALLED:
+                       return OsgiExplorerImages.INSTALLED;
+               case Bundle.RESOLVED:
+                       return OsgiExplorerImages.RESOLVED;
+               case Bundle.STARTING:
+                       return OsgiExplorerImages.STARTING;
+               case Bundle.STOPPING:
+                       return OsgiExplorerImages.STARTING;
+               case Bundle.ACTIVE:
+                       return OsgiExplorerImages.ACTIVE;
+               default:
+                       return null;
+               }
+       }
+
+       @Override
+       public String getText(Object element) {
+               return null;
+       }
+
+       @Override
+       public String getToolTipText(Object element) {
+               Bundle bundle = (Bundle) element;
+               Integer state = bundle.getState();
+               switch (state) {
+               case Bundle.UNINSTALLED:
+                       return "UNINSTALLED";
+               case Bundle.INSTALLED:
+                       return "INSTALLED";
+               case Bundle.RESOLVED:
+                       return "RESOLVED";
+               case Bundle.STARTING:
+                       String activationPolicy = bundle.getHeaders()
+                                       .get(Constants.BUNDLE_ACTIVATIONPOLICY).toString();
+
+                       // .get("Bundle-ActivationPolicy").toString();
+                       // FIXME constant triggers the compilation failure
+                       if (activationPolicy != null
+                                       && activationPolicy.equals(Constants.ACTIVATION_LAZY))
+                               // && activationPolicy.equals("lazy"))
+                               // FIXME constant triggers the compilation failure
+                               // && activationPolicy.equals(Constants.ACTIVATION_LAZY))
+                               return "<<LAZY>>";
+                       return "STARTING";
+               case Bundle.STOPPING:
+                       return "STOPPING";
+               case Bundle.ACTIVE:
+                       return "ACTIVE";
+               default:
+                       return null;
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/AdminLogView.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/AdminLogView.java
new file mode 100644 (file)
index 0000000..c76b890
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.useradmin;
+
+import java.util.ArrayList;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.node.ArgeoLogger;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.ui.part.ViewPart;
+
+/**
+ * Display log lines for all users with a virtual table.
+ */
+public class AdminLogView extends ViewPart {
+       public static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".adminLogView";
+
+       private TableViewer viewer;
+
+       private LogContentProvider logContentProvider;
+       private ArgeoLogger argeoLogger;
+
+       @Override
+       public void createPartControl(Composite parent) {
+               // FIXME doesn't return a monospace font in RAP
+               Font font = JFaceResources.getTextFont();
+               Table table = new Table(parent, SWT.VIRTUAL | SWT.MULTI | SWT.H_SCROLL
+                               | SWT.V_SCROLL | SWT.FULL_SELECTION | SWT.BORDER);
+               table.setFont(font);
+
+               viewer = new TableViewer(table);
+               viewer.setLabelProvider(new LabelProvider());
+               logContentProvider = new LogContentProvider(viewer) {
+                       private static final long serialVersionUID = -3401776448301180724L;
+
+                       @Override
+                       protected StringBuffer prefix(String username, Long timestamp,
+                                       String level, String category, String thread) {
+                               return super
+                                               .prefix(username, timestamp, level, category, thread)
+                                               .append(norm(level, 5))
+                                               .append(' ')
+                                               .append(norm(username != null ? username
+                                                               : "<anonymous>", 16)).append(' ');
+                       }
+               };
+               viewer.setContentProvider(logContentProvider);
+               // viewer.setUseHashlookup(true);
+               viewer.setInput(new ArrayList<String>());
+
+               if (argeoLogger != null)
+                       argeoLogger.registerForAll(logContentProvider, 1000, true);
+       }
+
+       @Override
+       public void setFocus() {
+               viewer.getTable().setFocus();
+       }
+
+       @Override
+       public void dispose() {
+               if (argeoLogger != null)
+                       argeoLogger.unregisterForAll(logContentProvider);
+       }
+
+       public void setArgeoLogger(ArgeoLogger argeoLogger) {
+               this.argeoLogger = argeoLogger;
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/LogContentProvider.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/LogContentProvider.java
new file mode 100644 (file)
index 0000000..24f3ca1
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.useradmin;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.argeo.node.ArgeoLogListener;
+import org.eclipse.jface.viewers.ILazyContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableItem;
+
+/** A content provider maintaining an array of lines */
+class LogContentProvider implements ILazyContentProvider, ArgeoLogListener {
+       private static final long serialVersionUID = -2084872367738339721L;
+
+       private DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
+
+       private final Long start;
+       /** current - start = line number. first line is number '1' */
+       private Long current;
+
+       // TODO make it configurable
+       private final Integer maxLineBufferSize = 10 * 1000;
+
+       private final TableViewer viewer;
+       private LinkedList<LogLine> lines;
+
+       public LogContentProvider(TableViewer viewer) {
+               this.viewer = viewer;
+               start = System.currentTimeMillis();
+               lines = new LinkedList<LogLine>();
+               current = start;
+       }
+
+       public synchronized void dispose() {
+               lines.clear();
+               lines = null;
+       }
+
+       @SuppressWarnings("unchecked")
+       public synchronized void inputChanged(Viewer viewer, Object oldInput,
+                       Object newInput) {
+               List<String> lin = (List<String>) newInput;
+               if (lin == null)
+                       return;
+               for (String line : lin) {
+                       addLine(line);
+               }
+               this.viewer.setItemCount(lines.size());
+       }
+
+       public void updateElement(int index) {
+               viewer.replace(lines.get(index), index);
+       }
+
+       public synchronized void appendLog(String username, Long timestamp,
+                       String level, String category, String thread, Object msg,
+                       String[] exception) {
+               // check if valid
+               if (lines == null)
+                       return;
+
+               String message = msg.toString();
+               int count = 0;
+               String prefix = prefix(username, timestamp, level, category, thread)
+                               .toString();
+               // String suffix = suffix(username, timestamp, level, category, thread);
+               for (String line : message.split("\n")) {
+                       addLine(count == 0 ? prefix + line : line);
+                       count++;
+               }
+
+               if (exception != null) {
+                       for (String ste : exception) {
+                               addLine(ste);
+                       }
+               }
+
+               viewer.getTable().getDisplay().asyncExec(new Runnable() {
+                       public void run() {
+                               if (lines == null)
+                                       return;
+                               viewer.setItemCount(lines.size());
+                               // doesn't work with syncExec
+                               scrollToLastLine();
+                       }
+               });
+       }
+
+       protected StringBuffer prefix(String username, Long timestamp,
+                       String level, String category, String thread) {
+               StringBuffer buf = new StringBuffer("");
+               buf.append(dateFormat.format(new Date(timestamp))).append(" ");
+               // buf.append(level).append(" ");
+               return buf;
+       }
+
+       /** Normalize string to the given size */
+       protected String norm(String str, Integer size) {
+               int length = str.length();
+               if (length == size)
+                       return str;
+               else if (length > size)
+                       return str.substring(0, size);
+               else {
+                       char[] arr = new char[size - length];
+                       Arrays.fill(arr, ' ');
+                       return str + new String(arr);
+               }
+       }
+
+       // protected String suffix(String username, Long timestamp, String level,
+       // String category, String thread) {
+       // return "";
+       // }
+
+       /** Scroll to the last line */
+       protected synchronized void scrollToLastLine() {
+               // we try to show last line with two methods
+               // viewer.reveal(lines.peekLast());
+
+               Table table = viewer.getTable();
+               TableItem ti = table.getItem(table.getItemCount() - 1);
+               table.showItem(ti);
+       }
+
+       protected synchronized LogLine addLine(String line) {
+               // check for maximal size and purge if necessary
+               while (lines.size() >= maxLineBufferSize) {
+                       for (int i = 0; i < maxLineBufferSize / 10; i++) {
+                               lines.poll();
+                       }
+               }
+
+               current++;
+               LogLine logLine = new LogLine(current, line);
+               lines.add(logLine);
+               return logLine;
+       }
+
+       private class LogLine {
+               private Long linenumber;
+               private String message;
+
+               public LogLine(Long linenumber, String message) {
+                       this.linenumber = linenumber;
+                       this.message = message;
+               }
+
+               @Override
+               public int hashCode() {
+                       return linenumber.intValue();
+               }
+
+               @Override
+               public boolean equals(Object obj) {
+                       if (obj instanceof LogLine)
+                               return ((LogLine) obj).linenumber.equals(linenumber);
+                       else
+                               return false;
+               }
+
+               @Override
+               public String toString() {
+                       return message;
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/LogView.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/LogView.java
new file mode 100644 (file)
index 0000000..07c808d
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.useradmin;
+
+import java.util.ArrayList;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.node.ArgeoLogListener;
+import org.argeo.node.ArgeoLogger;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.ui.part.ViewPart;
+
+/**
+ * Display log lines with a virtual table. Register and unregisters a
+ * {@link ArgeoLogListener} via OSGi services.
+ */
+public class LogView extends ViewPart {
+       public static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".logView";
+
+       private TableViewer viewer;
+
+       private LogContentProvider logContentProvider;
+       private ArgeoLogger argeoLogger;
+
+       @Override
+       public void createPartControl(Composite parent) {
+               Font font = JFaceResources.getTextFont();
+               Table table = new Table(parent, SWT.VIRTUAL | SWT.MULTI | SWT.H_SCROLL
+                               | SWT.V_SCROLL | SWT.FULL_SELECTION | SWT.BORDER);
+               table.setFont(font);
+
+               viewer = new TableViewer(table);
+               viewer.setLabelProvider(new LabelProvider());
+               logContentProvider = new LogContentProvider(viewer);
+               viewer.setContentProvider(logContentProvider);
+               // viewer.setUseHashlookup(true);
+               viewer.setInput(new ArrayList<String>());
+
+               if (argeoLogger != null)
+                       argeoLogger.register(logContentProvider, 1000);
+       }
+
+       @Override
+       public void setFocus() {
+               viewer.getTable().setFocus();
+       }
+
+       @Override
+       public void dispose() {
+               if (argeoLogger != null)
+                       argeoLogger.unregister(logContentProvider);
+       }
+
+       public void setArgeoLogger(ArgeoLogger argeoLogger) {
+               this.argeoLogger = argeoLogger;
+       }
+
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/UserProfile.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/useradmin/UserProfile.java
new file mode 100644 (file)
index 0000000..c86e9a0
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.useradmin;
+
+import java.util.TreeSet;
+
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.ui.part.ViewPart;
+
+/** Information about the currently logged in user */
+public class UserProfile extends ViewPart {
+       public static String ID = WorkbenchUiPlugin.PLUGIN_ID + ".userProfile";
+
+       private TableViewer viewer;
+
+       @Override
+       public void createPartControl(Composite parent) {
+               parent.setLayout(new GridLayout(2, false));
+
+               // Authentication authentication = CurrentUser.getAuthentication();
+               // EclipseUiUtils.createGridLL(parent, "Name", authentication
+               // .getPrincipal().toString());
+               EclipseUiUtils.createGridLL(parent, "User ID",
+                               CurrentUser.getUsername());
+
+               // roles table
+               Table table = new Table(parent, SWT.V_SCROLL | SWT.BORDER);
+               table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
+               table.setLinesVisible(false);
+               table.setHeaderVisible(false);
+               viewer = new TableViewer(table);
+               viewer.setContentProvider(new RolesContentProvider());
+               viewer.setLabelProvider(new LabelProvider());
+               getViewSite().setSelectionProvider(viewer);
+               viewer.setInput(getViewSite());
+       }
+
+       @Override
+       public void setFocus() {
+               viewer.getTable();
+       }
+
+       private class RolesContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = -4576917440167866233L;
+
+               public Object[] getElements(Object inputElement) {
+                       return new TreeSet<String>(CurrentUser.roles()).toArray();
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/util/CommandUtils.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/util/CommandUtils.java
new file mode 100644 (file)
index 0000000..b05ba07
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.argeo.cms.ui.workbench.WorkbenchUiPlugin;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.core.commands.Command;
+import org.eclipse.core.commands.Parameterization;
+import org.eclipse.core.commands.ParameterizedCommand;
+import org.eclipse.jface.action.IContributionItem;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.commands.ICommandService;
+import org.eclipse.ui.handlers.IHandlerService;
+import org.eclipse.ui.menus.CommandContributionItem;
+import org.eclipse.ui.menus.CommandContributionItemParameter;
+import org.eclipse.ui.services.IServiceLocator;
+
+/**
+ * Centralises useful and generic methods when dealing with commands in an
+ * Eclipse Workbench context
+ */
+public class CommandUtils {
+
+       /**
+        * Commodities the refresh of a single command with no parameter in a
+        * Menu.aboutToShow method to simplify further development
+        * 
+        * Note: that this method should be called with a false show command flag to
+        * remove a contribution that have been previously contributed
+        */
+       public static void refreshCommand(IMenuManager menuManager, IServiceLocator locator, String cmdId, String label,
+                       ImageDescriptor icon, boolean showCommand) {
+               refreshParameterizedCommand(menuManager, locator, cmdId, label, icon, showCommand, null);
+       }
+
+       /**
+        * Commodities the refresh the contribution of a command with a map of
+        * parameters in a context menu
+        * 
+        * The command ID is used has contribution item ID
+        */
+       public static void refreshParameterizedCommand(IMenuManager menuManager, IServiceLocator locator, String cmdId,
+                       String label, ImageDescriptor icon, boolean showCommand, Map<String, String> params) {
+               refreshParameterizedCommand(menuManager, locator, cmdId, cmdId, label, icon, showCommand, params);
+       }
+
+       /**
+        * Commodities the refresh the contribution of a command with a map of
+        * parameters in a context menu
+        * 
+        * @param menuManager
+        * @param locator
+        * @param contributionId
+        * @param commandId
+        * @param label
+        * @param icon
+        * @param showCommand
+        * @param params
+        */
+       public static void refreshParameterizedCommand(IMenuManager menuManager, IServiceLocator locator,
+                       String contributionId, String commandId, String label, ImageDescriptor icon, boolean showCommand,
+                       Map<String, String> params) {
+               IContributionItem ici = menuManager.find(contributionId);
+               if (ici != null)
+                       menuManager.remove(ici);
+               if (showCommand) {
+                       CommandContributionItemParameter contributionItemParameter = new CommandContributionItemParameter(locator,
+                                       null, commandId, SWT.PUSH);
+
+                       // Set Params
+                       contributionItemParameter.label = label;
+                       contributionItemParameter.icon = icon;
+
+                       if (params != null)
+                               contributionItemParameter.parameters = params;
+
+                       CommandContributionItem cci = new CommandContributionItem(contributionItemParameter);
+                       cci.setId(contributionId);
+                       menuManager.add(cci);
+               }
+       }
+
+       /** Helper to call a command without parameter easily */
+       public static void callCommand(String commandID) {
+               callCommand(commandID, null);
+       }
+
+       /** Helper to call a command with a single parameter easily */
+       public static void callCommand(String commandID, String parameterID, String parameterValue) {
+               Map<String, String> params = new HashMap<String, String>();
+               params.put(parameterID, parameterValue);
+               callCommand(commandID, params);
+       }
+
+       /**
+        * Helper to call a command with a map of parameters easily
+        * 
+        * @param paramMap
+        *            a map that links various command IDs with corresponding String
+        *            values.
+        */
+       public static void callCommand(String commandID, Map<String, String> paramMap) {
+               try {
+                       IWorkbench iw = WorkbenchUiPlugin.getDefault().getWorkbench();
+                       IHandlerService handlerService = (IHandlerService) iw.getService(IHandlerService.class);
+                       ICommandService cmdService = (ICommandService) iw.getActiveWorkbenchWindow()
+                                       .getService(ICommandService.class);
+                       Command cmd = cmdService.getCommand(commandID);
+
+                       ArrayList<Parameterization> parameters = null;
+                       ParameterizedCommand pc;
+
+                       if (paramMap != null) {
+                               // Set parameters of the command to launch :
+                               parameters = new ArrayList<Parameterization>();
+                               Parameterization parameterization;
+
+                               for (String id : paramMap.keySet()) {
+                                       parameterization = new Parameterization(cmd.getParameter(id), paramMap.get(id));
+                                       parameters.add(parameterization);
+                               }
+                               pc = new ParameterizedCommand(cmd, parameters.toArray(new Parameterization[parameters.size()]));
+                       } else
+                               pc = new ParameterizedCommand(cmd, null);
+
+                       // execute the command
+                       handlerService.executeCommand(pc, null);
+               } catch (Exception e) {
+                       throw new EclipseUiException("Unexpected error while" + " calling the command " + commandID, e);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/util/PrivilegedJob.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/util/PrivilegedJob.java
new file mode 100644 (file)
index 0000000..414fcba
--- /dev/null
@@ -0,0 +1,49 @@
+package org.argeo.cms.ui.workbench.util;
+
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+import javax.security.auth.Subject;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.jobs.Job;
+
+/**
+ * Propagate authentication to an eclipse job. Typically to execute a privileged
+ * action outside the UI thread
+ */
+public abstract class PrivilegedJob extends Job {
+       private final Subject subject;
+
+       public PrivilegedJob(String jobName) {
+               this(jobName, AccessController.getContext());
+       }
+
+       public PrivilegedJob(String jobName,
+                       AccessControlContext accessControlContext) {
+               super(jobName);
+               subject = Subject.getSubject(accessControlContext);
+
+               // Must be called *before* the job is scheduled,
+               // it is required for the progress window to appear
+               setUser(true);
+       }
+
+       @Override
+       protected IStatus run(final IProgressMonitor progressMonitor) {
+               PrivilegedAction<IStatus> privilegedAction = new PrivilegedAction<IStatus>() {
+                       public IStatus run() {
+                               return doRun(progressMonitor);
+                       }
+               };
+               return Subject.doAs(subject, privilegedAction);
+       }
+
+       /**
+        * Implement here what should be executed with default context
+        * authentication
+        */
+       protected abstract IStatus doRun(IProgressMonitor progressMonitor);
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/util/RolesSourceProvider.java b/org.argeo.cms.ui.workbench/src/org/argeo/cms/ui/workbench/util/RolesSourceProvider.java
new file mode 100644 (file)
index 0000000..f71c13d
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.workbench.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.argeo.cms.auth.CurrentUser;
+import org.eclipse.ui.AbstractSourceProvider;
+
+/**
+ * Provides the roles of the current user as a variable to be used for activity
+ * binding
+ */
+public class RolesSourceProvider extends AbstractSourceProvider {
+       public final static String ROLES_VARIABLE = "roles";
+       private final static String[] PROVIDED_SOURCE_NAMES = new String[] { ROLES_VARIABLE };
+
+       public Map<String, Set<String>> getCurrentState() {
+               Map<String, Set<String>> stateMap = new HashMap<String, Set<String>>();
+               stateMap.put(ROLES_VARIABLE, CurrentUser.roles());
+               return stateMap;
+       }
+
+       public String[] getProvidedSourceNames() {
+               return PROVIDED_SOURCE_NAMES;
+       }
+
+       public void updateRoles() {
+               fireSourceChanged(0, getCurrentState());
+       }
+
+       public void dispose() {
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/eclipse/spring/ApplicationContextTracker.java b/org.argeo.cms.ui.workbench/src/org/argeo/eclipse/spring/ApplicationContextTracker.java
new file mode 100644 (file)
index 0000000..1d3df43
--- /dev/null
@@ -0,0 +1,152 @@
+/*\r
+ * Copyright (C) 2007-2012 Argeo GmbH\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *         http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package org.argeo.eclipse.spring;\r
+\r
+import static java.text.MessageFormat.format;\r
+\r
+import org.apache.commons.logging.Log;\r
+import org.apache.commons.logging.LogFactory;\r
+import org.eclipse.core.runtime.Platform;\r
+import org.osgi.framework.Bundle;\r
+import org.osgi.framework.BundleContext;\r
+import org.osgi.framework.BundleException;\r
+import org.osgi.framework.FrameworkUtil;\r
+import org.osgi.framework.InvalidSyntaxException;\r
+import org.osgi.util.tracker.ServiceTracker;\r
+import org.springframework.context.ApplicationContext;\r
+\r
+/**\r
+ * Tracks Spring application context published as services.\r
+ * \r
+ * @author Heiko Seeberger\r
+ * @author Mathieu Baudier\r
+ */\r
+class ApplicationContextTracker {\r
+       private final static Log log = LogFactory\r
+                       .getLog(ApplicationContextTracker.class);\r
+\r
+       private static final String FILTER = "(&(objectClass=org.springframework.context.ApplicationContext)" //$NON-NLS-1$\r
+                       + "(org.springframework.context.service.name={0}))"; //$NON-NLS-1$\r
+\r
+       public final static String APPLICATION_CONTEXT_TRACKER_TIMEOUT = "org.argeo.eclipse.spring.applicationContextTrackerTimeout";\r
+\r
+       private static Long defaultTimeout = Long.parseLong(System.getProperty(\r
+                       APPLICATION_CONTEXT_TRACKER_TIMEOUT, "30000"));\r
+\r
+       @SuppressWarnings("rawtypes")\r
+       private ServiceTracker applicationContextServiceTracker;\r
+\r
+       /**\r
+        * @param contributorBundle\r
+        *            OSGi bundle for which the Spring application context is to be\r
+        *            tracked. Must not be null!\r
+        * @param factoryBundleContext\r
+        *            BundleContext object which can be used to track services\r
+        * @throws IllegalArgumentException\r
+        *             if the given bundle is null.\r
+        */\r
+       @SuppressWarnings({ "unchecked", "rawtypes" })\r
+       public ApplicationContextTracker(final Bundle contributorBundle,\r
+                       final BundleContext factoryBundleContext) {\r
+               final String filter = format(FILTER,\r
+                               contributorBundle.getSymbolicName());\r
+               try {\r
+                       applicationContextServiceTracker = new ServiceTracker(\r
+                                       factoryBundleContext, FrameworkUtil.createFilter(filter),\r
+                                       null);\r
+                       // applicationContextServiceTracker.open();\r
+               } catch (final InvalidSyntaxException e) {\r
+                       e.printStackTrace();\r
+               }\r
+       }\r
+\r
+       public void open() {\r
+               if (applicationContextServiceTracker != null) {\r
+                       applicationContextServiceTracker.open();\r
+               }\r
+       }\r
+\r
+       public void close() {\r
+               if (applicationContextServiceTracker != null) {\r
+                       applicationContextServiceTracker.close();\r
+               }\r
+       }\r
+\r
+       public ApplicationContext getApplicationContext() {\r
+               ApplicationContext applicationContext = null;\r
+               if (applicationContextServiceTracker != null) {\r
+                       try {\r
+                               applicationContext = (ApplicationContext) applicationContextServiceTracker\r
+                                               .waitForService(defaultTimeout);\r
+                       } catch (InterruptedException e) {\r
+                               e.printStackTrace();\r
+                       }\r
+               }\r
+               return applicationContext;\r
+       }\r
+\r
+       @Override\r
+       protected void finalize() throws Throwable {\r
+               close();\r
+               super.finalize();\r
+       }\r
+\r
+       static ApplicationContext getApplicationContext(String bundleSymbolicName) {\r
+               Bundle contributorBundle = Platform.getBundle(bundleSymbolicName);\r
+               return getApplicationContext(contributorBundle);\r
+       }\r
+\r
+       static ApplicationContext getApplicationContext(\r
+                       final Bundle contributorBundle) {\r
+               if (log.isTraceEnabled())\r
+                       log.trace("Get application context for bundle " + contributorBundle);\r
+\r
+               // Start if not yet started (also if in STARTING state, may be lazy)\r
+               if (contributorBundle.getState() != Bundle.ACTIVE) {\r
+                       if (log.isTraceEnabled())\r
+                               log.trace("Starting bundle: "\r
+                                               + contributorBundle.getSymbolicName());\r
+                       // Thread startBundle = new Thread("Start bundle "\r
+                       // + contributorBundle.getSymbolicName()) {\r
+                       // public void run() {\r
+                       try {\r
+                               contributorBundle.start();\r
+                       } catch (BundleException e) {\r
+                               log.error("Cannot start bundle " + contributorBundle, e);\r
+                       }\r
+                       // }\r
+                       // };\r
+                       // startBundle.start();\r
+                       // try {\r
+                       // startBundle.join(10 * 1000l);\r
+                       // } catch (InterruptedException e) {\r
+                       // // silent\r
+                       // }\r
+               }\r
+\r
+               final ApplicationContextTracker applicationContextTracker = new ApplicationContextTracker(\r
+                               contributorBundle, contributorBundle.getBundleContext());\r
+               ApplicationContext applicationContext = null;\r
+               try {\r
+                       applicationContextTracker.open();\r
+                       applicationContext = applicationContextTracker\r
+                                       .getApplicationContext();\r
+               } finally {\r
+                       applicationContextTracker.close();\r
+               }\r
+               return applicationContext;\r
+       }\r
+}\r
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/eclipse/spring/SpringCommandHandler.java b/org.argeo.cms.ui.workbench/src/org/argeo/eclipse/spring/SpringCommandHandler.java
new file mode 100644 (file)
index 0000000..698b937
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.spring;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.commands.IHandler;
+import org.eclipse.core.commands.IHandlerListener;
+import org.springframework.context.ApplicationContext;
+
+/** Allows to declare Eclipse commands as Spring beans */
+public class SpringCommandHandler implements IHandler {
+       private final static Log log = LogFactory
+                       .getLog(SpringCommandHandler.class);
+
+       public void addHandlerListener(IHandlerListener handlerListener) {
+       }
+
+       public void dispose() {
+       }
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               String commandId = event.getCommand().getId();
+               String bundleSymbolicName = commandId.substring(0,
+                               commandId.lastIndexOf('.'));
+               try {
+                       if (log.isTraceEnabled())
+                               log.trace("Execute " + event + " via spring command handler "
+                                               + this);
+                       // TODO: make it more flexible and robust
+                       ApplicationContext applicationContext = ApplicationContextTracker
+                                       .getApplicationContext(bundleSymbolicName);
+                       if (applicationContext == null)
+                               throw new EclipseUiException(
+                                               "No application context found for "
+                                                               + bundleSymbolicName);
+
+                       // retrieve the command via its id
+                       String beanName = event.getCommand().getId();
+
+                       if (!applicationContext.containsBean(beanName)) {
+                               if (beanName.startsWith(bundleSymbolicName))
+                                       beanName = beanName
+                                                       .substring(bundleSymbolicName.length() + 1);
+                       }
+
+                       if (!applicationContext.containsBean(beanName))
+                               throw new ExecutionException("No bean found with name "
+                                               + beanName + " in bundle " + bundleSymbolicName);
+                       Object bean = applicationContext.getBean(beanName);
+
+                       if (!(bean instanceof IHandler))
+                               throw new ExecutionException("Bean with name " + beanName
+                                               + " and class " + bean.getClass()
+                                               + " does not implement the IHandler interface.");
+
+                       IHandler handler = (IHandler) bean;
+                       return handler.execute(event);
+               } catch (Exception e) {
+                       // TODO: use eclipse error management
+                       // log.error(e);
+                       throw new ExecutionException("Cannot execute Spring command "
+                                       + commandId + " in bundle " + bundleSymbolicName, e);
+               }
+       }
+
+       public boolean isEnabled() {
+               return true;
+       }
+
+       public boolean isHandled() {
+               return true;
+       }
+
+       public void removeHandlerListener(IHandlerListener handlerListener) {
+       }
+}
diff --git a/org.argeo.cms.ui.workbench/src/org/argeo/eclipse/spring/SpringExtensionFactory.java b/org.argeo.cms.ui.workbench/src/org/argeo/eclipse/spring/SpringExtensionFactory.java
new file mode 100644 (file)
index 0000000..ab1e8ca
--- /dev/null
@@ -0,0 +1,113 @@
+/*\r
+ * Copyright (C) 2007-2012 Argeo GmbH\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *         http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package org.argeo.eclipse.spring;\r
+\r
+import org.argeo.eclipse.ui.EclipseUiException;\r
+import org.eclipse.core.runtime.CoreException;\r
+import org.eclipse.core.runtime.IConfigurationElement;\r
+import org.eclipse.core.runtime.IExecutableExtension;\r
+import org.eclipse.core.runtime.IExecutableExtensionFactory;\r
+import org.eclipse.core.runtime.IExtension;\r
+import org.springframework.context.ApplicationContext;\r
+\r
+/**\r
+ * The Spring Extension Factory builds a bridge between the Eclipse Extension\r
+ * Registry and the Spring Framework (especially Spring Dynamic Modules).\r
+ * \r
+ * It allows you to define your extension as a spring bean within the spring\r
+ * application context of your bundle. If you would like to use this bean as an\r
+ * instance of an extension (an Eclipse RCP view, for example) you define the\r
+ * extension with this spring extension factory as the class to be created.\r
+ * \r
+ * To let the spring extension factory pick the right bean from your application\r
+ * context you need to set the bean id to the same value as the id of the view\r
+ * within the view definition, for example. This is important if your extension\r
+ * definition contains more than one element, where each element has its own id.\r
+ * \r
+ * If the extension definition elements themselves have no id attribute the\r
+ * spring extension factory uses the id of the extension itself to identify the\r
+ * bean.\r
+ * \r
+ * original code from: <a href=\r
+ * "http://martinlippert.blogspot.com/2008/10/new-version-of-spring-extension-factory.html"\r
+ * >Blog entry</a>\r
+ * \r
+ * @author Martin Lippert\r
+ * @author mbaudier\r
+ */\r
+public class SpringExtensionFactory implements IExecutableExtensionFactory,\r
+               IExecutableExtension {\r
+\r
+       private Object bean;\r
+\r
+       public Object create() throws CoreException {\r
+               if (bean == null)\r
+                       throw new EclipseUiException("No underlying bean for extension");\r
+               return bean;\r
+       }\r
+\r
+       public void setInitializationData(IConfigurationElement config,\r
+                       String propertyName, Object data) throws CoreException {\r
+               String bundleSymbolicName = config.getContributor().getName();\r
+               ApplicationContext applicationContext = ApplicationContextTracker\r
+                               .getApplicationContext(bundleSymbolicName);\r
+               if (applicationContext == null)\r
+                       throw new EclipseUiException(\r
+                                       "Cannot find application context for bundle "\r
+                                                       + bundleSymbolicName);\r
+\r
+               String beanName = getBeanName(data, config);\r
+               if (beanName == null)\r
+                       throw new EclipseUiException("Cannot find bean name for extension "\r
+                                       + config);\r
+\r
+               if (!applicationContext.containsBean(beanName)) {\r
+                       if (beanName.startsWith(bundleSymbolicName))\r
+                               beanName = beanName.substring(bundleSymbolicName.length() + 1);\r
+               }\r
+\r
+               if (!applicationContext.containsBean(beanName))\r
+                       throw new EclipseUiException("No bean with name '" + beanName + "'");\r
+\r
+               this.bean = applicationContext.getBean(beanName);\r
+               if (this.bean instanceof IExecutableExtension) {\r
+                       ((IExecutableExtension) this.bean).setInitializationData(config,\r
+                                       propertyName, data);\r
+               }\r
+       }\r
+\r
+       private String getBeanName(Object data, IConfigurationElement config) {\r
+\r
+               // try the specific bean id the extension defines\r
+               if (data != null && data.toString().length() > 0) {\r
+                       return data.toString();\r
+               }\r
+\r
+               // try the id of the config element\r
+               if (config.getAttribute("id") != null) {\r
+                       return config.getAttribute("id");\r
+               }\r
+\r
+               // try the id of the extension element itself\r
+               if (config.getParent() != null\r
+                               && config.getParent() instanceof IExtension) {\r
+                       IExtension extensionDefinition = (IExtension) config.getParent();\r
+                       return extensionDefinition.getSimpleIdentifier();\r
+               }\r
+\r
+               return null;\r
+       }\r
+}\r
diff --git a/org.argeo.cms.ui/.classpath b/org.argeo.cms.ui/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms.ui/.gitignore b/org.argeo.cms.ui/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.cms.ui/.project b/org.argeo.cms.ui/.project
new file mode 100644 (file)
index 0000000..e52eb8e
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.ui</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.cms.ui/META-INF/.gitignore b/org.argeo.cms.ui/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.cms.ui/bnd.bnd b/org.argeo.cms.ui/bnd.bnd
new file mode 100644 (file)
index 0000000..5c9336c
--- /dev/null
@@ -0,0 +1,24 @@
+Bundle-Activator: org.argeo.cms.ui.internal.Activator
+Bundle-ActivationPolicy: lazy
+
+Import-Package: org.eclipse.swt,\
+org.eclipse.jface.window,\
+org.eclipse.core.commands,\
+javax.jcr.security,\
+org.argeo.eclipse.ui.dialogs,\
+org.eclipse.rap.fileupload;version="[2.1,4)",\
+org.eclipse.rap.rwt;version="[2.1,4)",\
+org.eclipse.rap.rwt.application;version="[2.1,4)",\
+org.eclipse.rap.rwt.client;version="[2.1,4)",\
+org.eclipse.rap.rwt.client.service;version="[2.1,4)",\
+org.eclipse.rap.rwt.service;version="[2.1,4)",\
+org.eclipse.rap.rwt.widgets;version="[2.1,4)",\
+org.osgi.*;version=0.0.0,\
+org.argeo.jcr.docbook,\
+*
+
+## TODO: in order to enable single sourcing, we have introduced dummy RAP packages 
+# in the RCP specific bundle org.argeo.eclipse.ui.rap.
+# this was working with RAP 2.x but since we upgrade Rap to 3.1.x, 
+# there is a version range issue cause org.argeo.eclipse.ui.rap bundle is still in 2.x
+# We enable large RAP version range as a workaround that must be fixed 
\ No newline at end of file
diff --git a/org.argeo.cms.ui/build.properties b/org.argeo.cms.ui/build.properties
new file mode 100644 (file)
index 0000000..c6baffa
--- /dev/null
@@ -0,0 +1,5 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .,\
+               icons/
diff --git a/org.argeo.cms.ui/icons/loading.gif b/org.argeo.cms.ui/icons/loading.gif
new file mode 100644 (file)
index 0000000..3288d10
Binary files /dev/null and b/org.argeo.cms.ui/icons/loading.gif differ
diff --git a/org.argeo.cms.ui/icons/noPic-goldenRatio-640px.png b/org.argeo.cms.ui/icons/noPic-goldenRatio-640px.png
new file mode 100644 (file)
index 0000000..0396506
Binary files /dev/null and b/org.argeo.cms.ui/icons/noPic-goldenRatio-640px.png differ
diff --git a/org.argeo.cms.ui/icons/noPic-square-640px.png b/org.argeo.cms.ui/icons/noPic-square-640px.png
new file mode 100644 (file)
index 0000000..8e3abb5
Binary files /dev/null and b/org.argeo.cms.ui/icons/noPic-square-640px.png differ
diff --git a/org.argeo.cms.ui/pom.xml b/org.argeo.cms.ui/pom.xml
new file mode 100644 (file)
index 0000000..f6ac176
--- /dev/null
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.cms.ui</artifactId>
+       <name>CMS UI</name>
+       <packaging>jar</packaging>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <!-- Specific -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <scope>provided</scope>
+               </dependency>
+
+               <!-- Theme -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui.theme</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- UI -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.rwt</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.commands</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.jface</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+
+               <!-- TODO move it to specific -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.filedialog</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.fileupload</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/forms/EditableLink.java b/org.argeo.cms.ui/src/org/argeo/cms/forms/EditableLink.java
new file mode 100644 (file)
index 0000000..ece0be3
--- /dev/null
@@ -0,0 +1,75 @@
+package org.argeo.cms.forms;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.viewers.EditablePart;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** Editable String that displays a browsable link when read-only */
+public class EditableLink extends EditablePropertyString implements
+               EditablePart {
+       private static final long serialVersionUID = 5055000749992803591L;
+
+       private String type;
+       private String message;
+       private boolean readOnly;
+
+       public EditableLink(Composite parent, int style, Node node,
+                       String propertyName, String type, String message)
+                       throws RepositoryException {
+               super(parent, style, node, propertyName, message);
+               this.message = message;
+               this.type = type;
+
+               readOnly = SWT.READ_ONLY == (style & SWT.READ_ONLY);
+               if (node.hasProperty(propertyName)) {
+                       this.setStyle(FormStyle.propertyText.style());
+                       this.setText(node.getProperty(propertyName).getString());
+               } else {
+                       this.setStyle(FormStyle.propertyMessage.style());
+                       this.setText("");
+               }
+       }
+
+       public void setText(String text) {
+               Control child = getControl();
+               if (child instanceof Label) {
+                       Label lbl = (Label) child;
+                       if (EclipseUiUtils.isEmpty(text))
+                               lbl.setText(message);
+                       else if (readOnly)
+                               setLinkValue(lbl, text);
+                       else
+                               // if canEdit() we put only the value with no link
+                               // to avoid glitches of the edition life cycle
+                               lbl.setText(text);
+               } else if (child instanceof Text) {
+                       Text txt = (Text) child;
+                       if (EclipseUiUtils.isEmpty(text)) {
+                               txt.setText("");
+                               txt.setMessage(message);
+                       } else
+                               txt.setText(text);
+               }
+       }
+
+       private void setLinkValue(Label lbl, String text) {
+               if (FormStyle.email.style().equals(type))
+                       lbl.setText(FormUtils.getMailLink(text));
+               else if (FormStyle.phone.style().equals(type))
+                       lbl.setText(FormUtils.getPhoneLink(text));
+               else if (FormStyle.website.style().equals(type))
+                       lbl.setText(FormUtils.getUrlLink(text));
+               else if (FormStyle.facebook.style().equals(type)
+                               || FormStyle.instagram.style().equals(type)
+                               || FormStyle.linkedIn.style().equals(type)
+                               || FormStyle.twitter.style().equals(type))
+                       lbl.setText(FormUtils.getUrlLink(text));
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/forms/EditableMultiStringProperty.java b/org.argeo.cms.ui/src/org/argeo/cms/forms/EditableMultiStringProperty.java
new file mode 100644 (file)
index 0000000..859f64b
--- /dev/null
@@ -0,0 +1,259 @@
+package org.argeo.cms.forms;
+
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.EditablePart;
+import org.argeo.cms.widgets.StyledControl;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.events.TraverseEvent;
+import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** Display, add or remove values from a list in a CMS context */
+public class EditableMultiStringProperty extends StyledControl implements EditablePart {
+       private static final long serialVersionUID = -7044614381252178595L;
+
+       private String propertyName;
+       private String message;
+       // TODO implement the ability to provide a list of possible values
+       private String[] possibleValues;
+       private boolean canEdit;
+       private SelectionListener removeValueSL;
+       private List<String> values;
+
+       // TODO manage within the CSS
+       private int rowSpacing = 5;
+       private int rowMarging = 0;
+       private int oneValueMargingRight = 5;
+       private int btnWidth = 16;
+       private int btnHeight = 16;
+       private int btnHorizontalIndent = 3;
+
+       public EditableMultiStringProperty(Composite parent, int style, Node node, String propertyName, List<String> values,
+                       String[] possibleValues, String addValueMsg, SelectionListener removeValueSelectionListener)
+                       throws RepositoryException {
+               super(parent, style, node, true);
+
+               this.propertyName = propertyName;
+               this.values = values;
+               this.possibleValues = new String[]{"Un", "Deux", "Trois"};
+               this.message = addValueMsg;
+               this.canEdit = removeValueSelectionListener != null;
+               this.removeValueSL = removeValueSelectionListener;
+       }
+
+       public List<String> getValues() {
+               return values;
+       }
+
+       public void setValues(List<String> values) {
+               this.values = values;
+       }
+
+       // Row layout items do not need explicit layout data
+       protected void setControlLayoutData(Control control) {
+       }
+
+       /** To be overridden */
+       protected void setContainerLayoutData(Composite composite) {
+               composite.setLayoutData(CmsUtils.fillWidth());
+       }
+
+       @Override
+       public Control getControl() {
+               return super.getControl();
+       }
+
+       @Override
+       protected Control createControl(Composite box, String style) {
+               Composite row = new Composite(box, SWT.NO_FOCUS);
+               row.setLayoutData(EclipseUiUtils.fillAll());
+
+               RowLayout rl = new RowLayout(SWT.HORIZONTAL);
+               rl.wrap = true;
+               rl.spacing = rowSpacing;
+               rl.marginRight = rl.marginLeft = rl.marginBottom = rl.marginTop = rowMarging;
+               row.setLayout(rl);
+
+               if (values != null) {
+                       for (final String value : values) {
+                               if (canEdit)
+                                       createRemovableValue(row, SWT.SINGLE, value);
+                               else
+                                       createValueLabel(row, SWT.SINGLE, value);
+                       }
+               }
+
+               if (!canEdit)
+                       return row;
+               else if (isEditing())
+                       return createText(row, style);
+               else
+                       return createLabel(row, style);
+       }
+
+       /**
+        * Override to provide specific layout for the existing values, typically
+        * adding a pound (#) char for tags or anchor info for browsable links. We
+        * assume the parent composite already has a layout and it is the caller
+        * responsibility to apply corresponding layout data
+        */
+       protected Label createValueLabel(Composite parent, int style, String value) {
+               Label label = new Label(parent, style);
+               label.setText("#" + value);
+               CmsUtils.markup(label);
+               CmsUtils.style(label, FormStyle.propertyText.style());
+               return label;
+       }
+
+       private Composite createRemovableValue(Composite parent, int style, String value) {
+               Composite valCmp = new Composite(parent, SWT.NO_FOCUS);
+               GridLayout gl = EclipseUiUtils.noSpaceGridLayout(new GridLayout(2, false));
+               gl.marginRight = oneValueMargingRight;
+               valCmp.setLayout(gl);
+
+               createValueLabel(valCmp, SWT.WRAP, value);
+
+               Button deleteBtn = new Button(valCmp, SWT.FLAT);
+               deleteBtn.setData(FormConstants.LINKED_VALUE, value);
+               deleteBtn.addSelectionListener(removeValueSL);
+               CmsUtils.style(deleteBtn, FormStyle.delete.style() + FormStyle.BUTTON_SUFFIX);
+               GridData gd = new GridData();
+               gd.heightHint = btnHeight;
+               gd.widthHint = btnWidth;
+               gd.horizontalIndent = btnHorizontalIndent;
+               deleteBtn.setLayoutData(gd);
+
+               return valCmp;
+       }
+
+       protected Text createText(Composite box, String style) {
+               final Text text = new Text(box, getStyle());
+               // The "add new value" text is not meant to change, so we can set it on
+               // creation
+               text.setMessage(message);
+               CmsUtils.style(text, style);
+               text.setFocus();
+
+               text.addTraverseListener(new TraverseListener() {
+                       private static final long serialVersionUID = 1L;
+
+                       public void keyTraversed(TraverseEvent e) {
+                               if (e.keyCode == SWT.CR) {
+                                       addValue(text);
+                                       e.doit = false;
+                               }
+                       }
+               });
+
+               // The OK button does not work with the focusOut listener
+               // because focus out is called before the OK button is pressed
+
+               // // we must call layout() now so that the row data can compute the
+               // height
+               // // of the other controls.
+               // text.getParent().layout();
+               // int height = text.getSize().y;
+               //
+               // Button okBtn = new Button(box, SWT.BORDER | SWT.PUSH | SWT.BOTTOM);
+               // okBtn.setText("OK");
+               // RowData rd = new RowData(SWT.DEFAULT, height - 2);
+               // okBtn.setLayoutData(rd);
+               //
+               // okBtn.addSelectionListener(new SelectionAdapter() {
+               // private static final long serialVersionUID = 2780819012423622369L;
+               //
+               // @Override
+               // public void widgetSelected(SelectionEvent e) {
+               // addValue(text);
+               // }
+               // });
+
+               return text;
+       }
+
+       /** Performs the real addition, overwrite to make further sanity checks */
+       protected void addValue(Text text) {
+               String value = text.getText();
+               String errMsg = null;
+
+               if (EclipseUiUtils.isEmpty(value))
+                       return;
+
+               if (values.contains(value))
+                       errMsg = "Dupplicated value: " + value + ", please correct and try again";
+               if (errMsg != null)
+                       MessageDialog.openError(this.getShell(), "Addition not allowed", errMsg);
+               else {
+                       values.add(value);
+                       Composite newCmp = createRemovableValue(text.getParent(), SWT.SINGLE, value);
+                       newCmp.moveAbove(text);
+                       text.setText("");
+                       newCmp.getParent().layout();
+               }
+       }
+
+       protected Label createLabel(Composite box, String style) {
+               if (canEdit) {
+                       Label lbl = new Label(box, getStyle());
+                       lbl.setText(message);
+                       CmsUtils.style(lbl, style);
+                       CmsUtils.markup(lbl);
+                       if (mouseListener != null)
+                               lbl.addMouseListener(mouseListener);
+                       return lbl;
+               }
+               return null;
+       }
+
+       protected void clear(boolean deep) {
+               Control child = getControl();
+               if (deep)
+                       super.clear(deep);
+               else {
+                       child.getParent().dispose();
+               }
+       }
+
+       public void setText(String text) {
+               Control child = getControl();
+               if (child instanceof Label) {
+                       Label lbl = (Label) child;
+                       if (canEdit)
+                               lbl.setText(text);
+                       else
+                               lbl.setText("");
+               } else if (child instanceof Text) {
+                       Text txt = (Text) child;
+                       txt.setText(text);
+               }
+       }
+
+       public synchronized void startEditing() {
+               getControl().setData(STYLE, FormStyle.propertyText.style());
+               super.startEditing();
+       }
+
+       public synchronized void stopEditing() {
+               getControl().setData(STYLE, FormStyle.propertyMessage.style());
+               super.stopEditing();
+       }
+
+       public String getPropertyName() {
+               return propertyName;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/forms/EditablePropertyDate.java b/org.argeo.cms.ui/src/org/argeo/cms/forms/EditablePropertyDate.java
new file mode 100644 (file)
index 0000000..928a285
--- /dev/null
@@ -0,0 +1,304 @@
+package org.argeo.cms.forms;
+
+import java.text.DateFormat;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.EditablePart;
+import org.argeo.cms.widgets.StyledControl;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.DateTime;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/** CMS form part to display and edit a date */
+public class EditablePropertyDate extends StyledControl implements EditablePart {
+       private static final long serialVersionUID = 2500215515778162468L;
+
+       // Context
+       private String propertyName;
+       private String message;
+       private DateFormat dateFormat;
+
+       // UI Objects
+       private Text dateTxt;
+       private Button openCalBtn;
+
+       // TODO manage within the CSS
+       private int fieldBtnSpacing = 5;
+
+       /**
+        * 
+        * @param parent
+        * @param style
+        * @param node
+        * @param propertyName
+        * @param message
+        * @param dateFormat
+        *            provide a {@link DateFormat} as contract to be able to
+        *            read/write dates as strings
+        * @throws RepositoryException
+        */
+       public EditablePropertyDate(Composite parent, int style, Node node,
+                       String propertyName, String message, DateFormat dateFormat)
+                       throws RepositoryException {
+               super(parent, style, node, false);
+
+               this.propertyName = propertyName;
+               this.message = message;
+               this.dateFormat = dateFormat;
+
+               if (node.hasProperty(propertyName)) {
+                       this.setStyle(FormStyle.propertyText.style());
+                       this.setText(dateFormat.format(node.getProperty(propertyName)
+                                       .getDate().getTime()));
+               } else {
+                       this.setStyle(FormStyle.propertyMessage.style());
+                       this.setText(message);
+               }
+       }
+
+       public void setText(String text) {
+               Control child = getControl();
+               if (child instanceof Label) {
+                       Label lbl = (Label) child;
+                       if (EclipseUiUtils.isEmpty(text))
+                               lbl.setText(message);
+                       else
+                               lbl.setText(text);
+               } else if (child instanceof Text) {
+                       Text txt = (Text) child;
+                       if (EclipseUiUtils.isEmpty(text)) {
+                               txt.setText("");
+                       } else
+                               txt.setText(text);
+               }
+       }
+
+       public synchronized void startEditing() {
+               // if (dateTxt != null && !dateTxt.isDisposed())
+               getControl().setData(STYLE, FormStyle.propertyText.style());
+               super.startEditing();
+       }
+
+       public synchronized void stopEditing() {
+               if (EclipseUiUtils.isEmpty(dateTxt.getText()))
+                       getControl().setData(STYLE, FormStyle.propertyMessage.style());
+               else
+                       getControl().setData(STYLE, FormStyle.propertyText.style());
+               super.stopEditing();
+       }
+
+       public String getPropertyName() {
+               return propertyName;
+       }
+
+       @Override
+       protected Control createControl(Composite box, String style) {
+               if (isEditing()) {
+                       return createCustomEditableControl(box, style);
+               } else
+                       return createLabel(box, style);
+       }
+
+       protected Label createLabel(Composite box, String style) {
+               Label lbl = new Label(box, getStyle() | SWT.WRAP);
+               lbl.setLayoutData(CmsUtils.fillWidth());
+               CmsUtils.style(lbl, style);
+               CmsUtils.markup(lbl);
+               if (mouseListener != null)
+                       lbl.addMouseListener(mouseListener);
+               return lbl;
+       }
+
+       private Control createCustomEditableControl(Composite box, String style) {
+               box.setLayoutData(CmsUtils.fillWidth());
+               Composite dateComposite = new Composite(box, SWT.NONE);
+               GridLayout gl = EclipseUiUtils.noSpaceGridLayout(new GridLayout(2,
+                               false));
+               gl.horizontalSpacing = fieldBtnSpacing;
+               dateComposite.setLayout(gl);
+               dateTxt = new Text(dateComposite, SWT.BORDER);
+               CmsUtils.style(dateTxt, style);
+               dateTxt.setLayoutData(new GridData(120, SWT.DEFAULT));
+               dateTxt.setToolTipText("Enter a date with form \""
+                               + FormUtils.DEFAULT_SHORT_DATE_FORMAT
+                               + "\" or use the calendar");
+               openCalBtn = new Button(dateComposite, SWT.FLAT);
+               CmsUtils.style(openCalBtn, FormStyle.calendar.style()
+                               + FormStyle.BUTTON_SUFFIX);
+               GridData gd = new GridData(SWT.CENTER, SWT.CENTER, false, false);
+               gd.heightHint = 17;
+               openCalBtn.setLayoutData(gd);
+               // openCalBtn.setImage(PeopleRapImages.CALENDAR_BTN);
+
+               openCalBtn.addSelectionListener(new SelectionAdapter() {
+                       private static final long serialVersionUID = 1L;
+
+                       public void widgetSelected(SelectionEvent event) {
+                               CalendarPopup popup = new CalendarPopup(dateTxt);
+                               popup.open();
+                       }
+               });
+
+               // dateTxt.addFocusListener(new FocusListener() {
+               // private static final long serialVersionUID = 1L;
+               //
+               // @Override
+               // public void focusLost(FocusEvent event) {
+               // String newVal = dateTxt.getText();
+               // // Enable reset of the field
+               // if (FormUtils.notNull(newVal))
+               // calendar = null;
+               // else {
+               // try {
+               // Calendar newCal = parseDate(newVal);
+               // // DateText.this.setText(newCal);
+               // calendar = newCal;
+               // } catch (ParseException pe) {
+               // // Silent. Manage error popup?
+               // if (calendar != null)
+               // EditablePropertyDate.this.setText(calendar);
+               // }
+               // }
+               // }
+               //
+               // @Override
+               // public void focusGained(FocusEvent event) {
+               // }
+               // });
+               return dateTxt;
+       }
+
+       protected void clear(boolean deep) {
+               Control child = getControl();
+               if (deep || child instanceof Label)
+                       super.clear(deep);
+               else {
+                       child.getParent().dispose();
+               }
+       }
+
+       /** Enable setting a custom tooltip on the underlying text */
+       @Deprecated
+       public void setToolTipText(String toolTipText) {
+               dateTxt.setToolTipText(toolTipText);
+       }
+
+       @Deprecated
+       /** Enable setting a custom message on the underlying text */
+       public void setMessage(String message) {
+               dateTxt.setMessage(message);
+       }
+
+       @Deprecated
+       public void setText(Calendar cal) {
+               String newValueStr = "";
+               if (cal != null)
+                       newValueStr = dateFormat.format(cal.getTime());
+               if (!newValueStr.equals(dateTxt.getText()))
+                       dateTxt.setText(newValueStr);
+       }
+
+       // UTILITIES TO MANAGE THE CALENDAR POPUP
+       // TODO manage the popup shell in a cleaner way
+       private class CalendarPopup extends Shell {
+               private static final long serialVersionUID = 1L;
+               private DateTime dateTimeCtl;
+
+               public CalendarPopup(Control source) {
+                       super(source.getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+                       populate();
+                       // Add border and shadow style
+                       CmsUtils.markup(CalendarPopup.this);
+                       CmsUtils.style(CalendarPopup.this, FormStyle.popupCalendar.style());
+                       pack();
+                       layout();
+                       setLocation(source.toDisplay((source.getLocation().x - 2),
+                                       (source.getSize().y) + 3));
+
+                       addShellListener(new ShellAdapter() {
+                               private static final long serialVersionUID = 5178980294808435833L;
+
+                               @Override
+                               public void shellDeactivated(ShellEvent e) {
+                                       close();
+                                       dispose();
+                               }
+                       });
+                       open();
+               }
+
+               private void setProperty() {
+                       // Direct set does not seems to work. investigate
+                       // cal.set(dateTimeCtl.getYear(), dateTimeCtl.getMonth(),
+                       // dateTimeCtl.getDay(), 12, 0);
+                       Calendar cal = new GregorianCalendar();
+                       cal.set(Calendar.YEAR, dateTimeCtl.getYear());
+                       cal.set(Calendar.MONTH, dateTimeCtl.getMonth());
+                       cal.set(Calendar.DAY_OF_MONTH, dateTimeCtl.getDay());
+                       String dateStr = dateFormat.format(cal.getTime());
+                       dateTxt.setText(dateStr);
+               }
+
+               protected void populate() {
+                       setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       dateTimeCtl = new DateTime(this, SWT.CALENDAR);
+                       dateTimeCtl.setLayoutData(EclipseUiUtils.fillAll());
+
+                       Calendar calendar = FormUtils.parseDate(dateFormat,
+                                       dateTxt.getText());
+
+                       if (calendar != null)
+                               dateTimeCtl.setDate(calendar.get(Calendar.YEAR),
+                                               calendar.get(Calendar.MONTH),
+                                               calendar.get(Calendar.DAY_OF_MONTH));
+
+                       dateTimeCtl.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = -8414377364434281112L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       setProperty();
+                               }
+                       });
+
+                       dateTimeCtl.addMouseListener(new MouseListener() {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public void mouseUp(MouseEvent e) {
+                               }
+
+                               @Override
+                               public void mouseDown(MouseEvent e) {
+                               }
+
+                               @Override
+                               public void mouseDoubleClick(MouseEvent e) {
+                                       setProperty();
+                                       close();
+                                       dispose();
+                               }
+                       });
+               }
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/forms/EditablePropertyString.java b/org.argeo.cms.ui/src/org/argeo/cms/forms/EditablePropertyString.java
new file mode 100644 (file)
index 0000000..dd3ff29
--- /dev/null
@@ -0,0 +1,80 @@
+package org.argeo.cms.forms;
+
+import static org.argeo.cms.forms.FormStyle.propertyMessage;
+import static org.argeo.cms.forms.FormStyle.propertyText;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.viewers.EditablePart;
+import org.argeo.cms.widgets.EditableText;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** Editable String in a CMS context */
+public class EditablePropertyString extends EditableText implements
+               EditablePart {
+       private static final long serialVersionUID = 5055000749992803591L;
+
+       private String propertyName;
+       private String message;
+
+       // encode the '&' character in rap
+       private final static String AMPERSAND = "&#38;";
+       private final static String AMPERSAND_REGEX = "&(?![#a-zA-Z0-9]+;)";
+
+       public EditablePropertyString(Composite parent, int style, Node node,
+                       String propertyName, String message) throws RepositoryException {
+               super(parent, style, node, true);
+
+               this.propertyName = propertyName;
+               this.message = message;
+
+               if (node.hasProperty(propertyName)) {
+                       this.setStyle(propertyText.style());
+                       this.setText(node.getProperty(propertyName).getString());
+               } else {
+                       this.setStyle(propertyMessage.style());
+                       this.setText(message + "  ");
+               }
+       }
+
+       public void setText(String text) {
+               Control child = getControl();
+               if (child instanceof Label) {
+                       Label lbl = (Label) child;
+                       if (EclipseUiUtils.isEmpty(text))
+                               lbl.setText(message + "  ");
+                       else
+                               // TODO enhance this
+                               lbl.setText(text.replaceAll(AMPERSAND_REGEX, AMPERSAND));
+               } else if (child instanceof Text) {
+                       Text txt = (Text) child;
+                       if (EclipseUiUtils.isEmpty(text)) {
+                               txt.setText("");
+                               txt.setMessage(message + " ");
+                       } else
+                               txt.setText(text.replaceAll("<br/>", "\n"));
+               }
+       }
+
+       public synchronized void startEditing() {
+               getControl().setData(STYLE, propertyText.style());
+               super.startEditing();
+       }
+
+       public synchronized void stopEditing() {
+               if (EclipseUiUtils.isEmpty(((Text) getControl()).getText()))
+                       getControl().setData(STYLE, propertyMessage.style());
+               else
+                       getControl().setData(STYLE, propertyText.style());
+               super.stopEditing();
+       }
+
+       public String getPropertyName() {
+               return propertyName;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/forms/FormConstants.java b/org.argeo.cms.ui/src/org/argeo/cms/forms/FormConstants.java
new file mode 100644 (file)
index 0000000..18df3e4
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.cms.forms;
+
+/** Constants used in the various CMS Forms */
+public interface FormConstants {
+       // DATAKEYS
+       public final static String LINKED_VALUE = "LinkedValue";
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/forms/FormEditorHeader.java b/org.argeo.cms.ui/src/org/argeo/cms/forms/FormEditorHeader.java
new file mode 100644 (file)
index 0000000..92ce9da
--- /dev/null
@@ -0,0 +1,114 @@
+package org.argeo.cms.forms;
+
+import java.util.Observable;
+import java.util.Observer;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+
+/** Add life cycle management abilities to an editable form page */
+public class FormEditorHeader implements SelectionListener, Observer {
+       private static final long serialVersionUID = 7392898696542484282L;
+
+       // private final Node context;
+       private final CmsEditable cmsEditable;
+       private Button publishBtn;
+
+       // Should we provide here the ability to switch from read only to edition
+       // mode?
+       // private Button editBtn;
+       // private boolean readOnly;
+
+       // TODO add information about the current node status, typically if it is
+       // dirty or not
+
+       private Composite parent;
+       private Composite display;
+       private Object layoutData;
+
+       public FormEditorHeader(Composite parent, int style, Node context,
+                       CmsEditable cmsEditable) {
+               this.cmsEditable = cmsEditable;
+               this.parent = parent;
+               // readOnly = SWT.READ_ONLY == (style & SWT.READ_ONLY);
+               // this.context = context;
+               if (this.cmsEditable instanceof Observable)
+                       ((Observable) this.cmsEditable).addObserver(this);
+               refresh();
+       }
+
+       public void setLayoutData(Object layoutData) {
+               this.layoutData = layoutData;
+               if (display != null && !display.isDisposed())
+                       display.setLayoutData(layoutData);
+       }
+
+       protected void refresh() {
+               if (display != null && !display.isDisposed())
+                       display.dispose();
+
+               display = new Composite(parent, SWT.NONE);
+               display.setLayoutData(layoutData);
+
+               CmsUtils.style(display, FormStyle.header.style());
+               display.setBackgroundMode(SWT.INHERIT_FORCE);
+
+               display.setLayout(CmsUtils.noSpaceGridLayout());
+
+               publishBtn = createSimpleBtn(display, getPublishButtonLabel());
+               display.moveAbove(null);
+               parent.layout();
+       }
+
+       private Button createSimpleBtn(Composite parent, String label) {
+               Button button = new Button(parent, SWT.FLAT | SWT.PUSH);
+               button.setText(label);
+               CmsUtils.style(button, FormStyle.header.style());
+               button.addSelectionListener(this);
+               return button;
+       }
+
+       private String getPublishButtonLabel() {
+               // Rather check if the current node differs from what has been
+               // previously committed
+               // For the time being, we always reach here, the underlying CmsEditable
+               // is always editing.
+               if (cmsEditable.isEditing())
+                       return " Publish ";
+               else
+                       return " Edit ";
+       }
+
+       @Override
+       public void widgetSelected(SelectionEvent e) {
+               if (e.getSource() == publishBtn) {
+                       // For the time being, the underlying CmsEditable
+                       // is always editing when we reach this point
+                       if (cmsEditable.isEditing()) {
+                               // we always leave the node in a check outed state
+                               cmsEditable.stopEditing();
+                               cmsEditable.startEditing();
+                       } else {
+                               cmsEditable.startEditing();
+                       }
+               }
+       }
+
+       @Override
+       public void widgetDefaultSelected(SelectionEvent e) {
+       }
+
+       @Override
+       public void update(Observable o, Object arg) {
+               if (o == cmsEditable) {
+                       refresh();
+               }
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/forms/FormPageViewer.java b/org.argeo.cms.ui/src/org/argeo/cms/forms/FormPageViewer.java
new file mode 100644 (file)
index 0000000..75e0e76
--- /dev/null
@@ -0,0 +1,613 @@
+package org.argeo.cms.forms;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.text.Img;
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.ui.CmsImageManager;
+import org.argeo.cms.ui.internal.text.MarkupValidatorCopy;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.AbstractPageViewer;
+import org.argeo.cms.viewers.EditablePart;
+import org.argeo.cms.viewers.Section;
+import org.argeo.cms.viewers.SectionPart;
+import org.argeo.cms.widgets.EditableImage;
+import org.argeo.cms.widgets.StyledControl;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.rap.fileupload.FileDetails;
+import org.eclipse.rap.fileupload.FileUploadEvent;
+import org.eclipse.rap.fileupload.FileUploadHandler;
+import org.eclipse.rap.fileupload.FileUploadListener;
+import org.eclipse.rap.fileupload.FileUploadReceiver;
+import org.eclipse.rap.rwt.service.ServerPushSession;
+import org.eclipse.rap.rwt.widgets.FileUpload;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** Manage life cycle of a form page that is linked to a given node */
+public class FormPageViewer extends AbstractPageViewer {
+       private final static Log log = LogFactory.getLog(FormPageViewer.class);
+       private static final long serialVersionUID = 5277789504209413500L;
+
+       private final Section mainSection;
+
+       // TODO manage within the CSS
+       private int labelColWidth = 150;
+       private int rowLayoutHSpacing = 8;
+
+       // Context cached in the viewer
+       // The reference to translate from text to calendar and reverse
+       private DateFormat dateFormat = new SimpleDateFormat(FormUtils.DEFAULT_SHORT_DATE_FORMAT);
+       private CmsImageManager imageManager;
+       private FileUploadListener fileUploadListener;
+
+       public FormPageViewer(Section mainSection, int style, CmsEditable cmsEditable) throws RepositoryException {
+               super(mainSection, style, cmsEditable);
+               this.mainSection = mainSection;
+
+               if (getCmsEditable().canEdit()) {
+                       fileUploadListener = new FUL();
+               }
+       }
+
+       @Override
+       protected void prepare(EditablePart part, Object caretPosition) {
+               if (part instanceof Img) {
+                       ((Img) part).setFileUploadListener(fileUploadListener);
+               }
+       }
+
+       /** To be overridden.Save the edited part. */
+       protected void save(EditablePart part) throws RepositoryException {
+               Node node = null;
+               if (part instanceof EditableMultiStringProperty) {
+                       EditableMultiStringProperty ept = (EditableMultiStringProperty) part;
+                       // SWT : View
+                       List<String> values = ept.getValues();
+                       // JCR : Model
+                       node = ept.getNode();
+                       String propName = ept.getPropertyName();
+                       if (values.isEmpty()) {
+                               if (node.hasProperty(propName))
+                                       node.getProperty(propName).remove();
+                       } else {
+                               node.setProperty(propName, values.toArray(new String[0]));
+                       }
+                       // => Viewer : Controller
+               } else if (part instanceof EditablePropertyString) {
+                       EditablePropertyString ept = (EditablePropertyString) part;
+                       // SWT : View
+                       String txt = ((Text) ept.getControl()).getText();
+                       // JCR : Model
+                       node = ept.getNode();
+                       String propName = ept.getPropertyName();
+                       if (EclipseUiUtils.isEmpty(txt)) {
+                               if (node.hasProperty(propName))
+                                       node.getProperty(propName).remove();
+                       } else {
+                               setPropertySilently(node, propName, txt);
+                               // node.setProperty(propName, txt);
+                       }
+                       // node.getSession().save();
+                       // => Viewer : Controller
+               } else if (part instanceof EditablePropertyDate) {
+                       EditablePropertyDate ept = (EditablePropertyDate) part;
+                       Calendar cal = FormUtils.parseDate(dateFormat, ((Text) ept.getControl()).getText());
+                       node = ept.getNode();
+                       String propName = ept.getPropertyName();
+                       if (cal == null) {
+                               if (node.hasProperty(propName))
+                                       node.getProperty(propName).remove();
+                       } else {
+                               node.setProperty(propName, cal);
+                       }
+                       // node.getSession().save();
+                       // => Viewer : Controller
+               }
+               // TODO: make this configurable, sometimes we do not want to save the
+               // current session at this stage
+               if (node != null && node.getSession().hasPendingChanges()) {
+                       JcrUtils.updateLastModified(node);
+                       node.getSession().save();
+               }
+       }
+
+       @Override
+       protected void updateContent(EditablePart part) throws RepositoryException {
+               if (part instanceof EditableMultiStringProperty) {
+                       EditableMultiStringProperty ept = (EditableMultiStringProperty) part;
+                       // SWT : View
+                       Node node = ept.getNode();
+                       String propName = ept.getPropertyName();
+                       List<String> valStrings = new ArrayList<String>();
+                       if (node.hasProperty(propName)) {
+                               Value[] values = node.getProperty(propName).getValues();
+                               for (Value val : values)
+                                       valStrings.add(val.getString());
+                       }
+                       ept.setValues(valStrings);
+               } else if (part instanceof EditablePropertyString) {
+                       // || part instanceof EditableLink
+                       EditablePropertyString ept = (EditablePropertyString) part;
+                       // JCR : Model
+                       Node node = ept.getNode();
+                       String propName = ept.getPropertyName();
+                       if (node.hasProperty(propName)) {
+                               String value = node.getProperty(propName).getString();
+                               ept.setText(value);
+                       } else
+                               ept.setText("");
+                       // => Viewer : Controller
+               } else if (part instanceof EditablePropertyDate) {
+                       EditablePropertyDate ept = (EditablePropertyDate) part;
+                       // JCR : Model
+                       Node node = ept.getNode();
+                       String propName = ept.getPropertyName();
+                       if (node.hasProperty(propName))
+                               ept.setText(dateFormat.format(node.getProperty(propName).getDate().getTime()));
+                       else
+                               ept.setText("");
+               } else if (part instanceof SectionPart) {
+                       SectionPart sectionPart = (SectionPart) part;
+                       Node partNode = sectionPart.getNode();
+                       // use control AFTER setting style, since it may have been reset
+                       if (part instanceof EditableImage) {
+                               EditableImage editableImage = (EditableImage) part;
+                               imageManager().load(partNode, part.getControl(), editableImage.getPreferredImageSize());
+                       }
+               }
+       }
+
+       // FILE UPLOAD LISTENER
+       protected class FUL implements FileUploadListener {
+
+               public FUL() {
+               }
+
+               public void uploadProgress(FileUploadEvent event) {
+                       // TODO Monitor upload progress
+               }
+
+               public void uploadFailed(FileUploadEvent event) {
+                       throw new CmsException("Upload failed " + event, event.getException());
+               }
+
+               public void uploadFinished(FileUploadEvent event) {
+                       for (FileDetails file : event.getFileDetails()) {
+                               if (log.isDebugEnabled())
+                                       log.debug("Received: " + file.getFileName());
+                       }
+                       mainSection.getDisplay().syncExec(new Runnable() {
+                               @Override
+                               public void run() {
+                                       saveEdit();
+                               }
+                       });
+                       FileUploadHandler uploadHandler = (FileUploadHandler) event.getSource();
+                       uploadHandler.dispose();
+               }
+       }
+
+       // FOCUS OUT LISTENER
+       protected FocusListener createFocusListener() {
+               return new FocusOutListener();
+       }
+
+       private class FocusOutListener implements FocusListener {
+               private static final long serialVersionUID = -6069205786732354186L;
+
+               @Override
+               public void focusLost(FocusEvent event) {
+                       saveEdit();
+               }
+
+               @Override
+               public void focusGained(FocusEvent event) {
+                       // does nothing;
+               }
+       }
+
+       // MOUSE LISTENER
+       @Override
+       protected MouseListener createMouseListener() {
+               return new ML();
+       }
+
+       private class ML extends MouseAdapter {
+               private static final long serialVersionUID = 8526890859876770905L;
+
+               @Override
+               public void mouseDoubleClick(MouseEvent e) {
+                       if (e.button == 1) {
+                               Control source = (Control) e.getSource();
+                               if (getCmsEditable().canEdit()) {
+                                       if (getCmsEditable().isEditing() && !(getEdited() instanceof Img)) {
+                                               if (source == mainSection)
+                                                       return;
+                                               EditablePart part = findDataParent(source);
+                                               upload(part);
+                                       } else {
+                                               getCmsEditable().startEditing();
+                                       }
+                               }
+                       }
+               }
+
+               @Override
+               public void mouseDown(MouseEvent e) {
+                       if (getCmsEditable().isEditing()) {
+                               if (e.button == 1) {
+                                       Control source = (Control) e.getSource();
+                                       EditablePart composite = findDataParent(source);
+                                       Point point = new Point(e.x, e.y);
+                                       if (!(composite instanceof Img))
+                                               edit(composite, source.toDisplay(point));
+                               } else if (e.button == 3) {
+                                       // EditablePart composite = findDataParent((Control) e
+                                       // .getSource());
+                                       // if (styledTools != null)
+                                       // styledTools.show(composite, new Point(e.x, e.y));
+                               }
+                       }
+               }
+
+               protected synchronized void upload(EditablePart part) {
+                       if (part instanceof SectionPart) {
+                               if (part instanceof Img) {
+                                       if (getEdited() == part)
+                                               return;
+                                       edit(part, null);
+                                       layout(part.getControl());
+                               }
+                       }
+               }
+       }
+
+       @Override
+       public Control getControl() {
+               return mainSection;
+       }
+
+       protected CmsImageManager imageManager() {
+               if (imageManager == null)
+                       imageManager = CmsUtils.getCmsView().getImageManager();
+               return imageManager;
+       }
+
+       // LOCAL UI HELPERS
+       protected Section createSectionIfNeeded(Composite body, Node node) throws RepositoryException {
+               Section section = null;
+               if (node != null) {
+                       section = new Section(body, SWT.NO_FOCUS, node);
+                       section.setLayoutData(CmsUtils.fillWidth());
+                       section.setLayout(CmsUtils.noSpaceGridLayout());
+               }
+               return section;
+       }
+
+       protected void createSimpleLT(Composite bodyRow, Node node, String propName, String label, String msg)
+                       throws RepositoryException {
+               if (getCmsEditable().canEdit() || node.hasProperty(propName)) {
+                       createPropertyLbl(bodyRow, label);
+                       EditablePropertyString eps = new EditablePropertyString(bodyRow, SWT.WRAP | SWT.LEFT, node, propName, msg);
+                       eps.setMouseListener(getMouseListener());
+                       eps.setFocusListener(getFocusListener());
+                       eps.setLayoutData(CmsUtils.fillWidth());
+               }
+       }
+
+       protected void createMultiStringLT(Composite bodyRow, Node node, String propName, String label, String msg)
+                       throws RepositoryException {
+               boolean canEdit = getCmsEditable().canEdit();
+               if (canEdit || node.hasProperty(propName)) {
+                       createPropertyLbl(bodyRow, label);
+
+                       List<String> valueStrings = new ArrayList<String>();
+
+                       if (node.hasProperty(propName)) {
+                               Value[] values = node.getProperty(propName).getValues();
+                               for (Value value : values)
+                                       valueStrings.add(value.getString());
+                       }
+
+                       // TODO use a drop down to display possible values to the end user
+                       EditableMultiStringProperty emsp = new EditableMultiStringProperty(bodyRow, SWT.SINGLE | SWT.LEAD, node,
+                                       propName, valueStrings, new String[] { "Implement this" }, msg,
+                                       canEdit ? getRemoveValueSelListener() : null);
+                       addListeners(emsp);
+                       // emsp.setMouseListener(getMouseListener());
+                       emsp.setStyle(FormStyle.propertyMessage.style());
+                       emsp.setLayoutData(CmsUtils.fillWidth());
+               }
+       }
+
+       protected Label createPropertyLbl(Composite parent, String value) {
+               return createPropertyLbl(parent, value, SWT.TOP);
+       }
+
+       protected Label createPropertyLbl(Composite parent, String value, int vAlign) {
+               boolean isSmall = CmsUtils.getCmsView().getUxContext().isSmall();
+               Label label = new Label(parent, isSmall ? SWT.LEFT : SWT.RIGHT | SWT.WRAP);
+               label.setText(value + " ");
+               CmsUtils.style(label, FormStyle.propertyLabel.style());
+               GridData gd = new GridData(isSmall ? SWT.LEFT : SWT.RIGHT, vAlign, false, false);
+               gd.widthHint = labelColWidth;
+               label.setLayoutData(gd);
+               return label;
+       }
+
+       protected Label newStyledLabel(Composite parent, String style, String value) {
+               Label label = new Label(parent, SWT.NONE);
+               label.setText(value);
+               CmsUtils.style(label, style);
+               return label;
+       }
+
+       protected Composite createRowLayoutComposite(Composite parent) throws RepositoryException {
+               Composite bodyRow = new Composite(parent, SWT.NO_FOCUS);
+               bodyRow.setLayoutData(CmsUtils.fillWidth());
+               RowLayout rl = new RowLayout(SWT.WRAP);
+               rl.type = SWT.HORIZONTAL;
+               rl.spacing = rowLayoutHSpacing;
+               rl.marginHeight = rl.marginWidth = 0;
+               rl.marginTop = rl.marginBottom = rl.marginLeft = rl.marginRight = 0;
+               bodyRow.setLayout(rl);
+               return bodyRow;
+       }
+
+       protected Composite createAddImgComposite(final Section section, Composite parent, final Node parentNode)
+                       throws RepositoryException {
+
+               Composite body = new Composite(parent, SWT.NO_FOCUS);
+               body.setLayout(new GridLayout());
+
+               FormFileUploadReceiver receiver = new FormFileUploadReceiver(section, parentNode, null);
+               final FileUploadHandler currentUploadHandler = new FileUploadHandler(receiver);
+               if (fileUploadListener != null)
+                       currentUploadHandler.addUploadListener(fileUploadListener);
+
+               // Button creation
+               final FileUpload fileUpload = new FileUpload(body, SWT.BORDER);
+               fileUpload.setText("Import an image");
+               fileUpload.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
+               fileUpload.addSelectionListener(new SelectionAdapter() {
+                       private static final long serialVersionUID = 4869523412991968759L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               ServerPushSession pushSession = new ServerPushSession();
+                               pushSession.start();
+                               String uploadURL = currentUploadHandler.getUploadUrl();
+                               fileUpload.submit(uploadURL);
+                       }
+               });
+
+               return body;
+       }
+
+       protected class FormFileUploadReceiver extends FileUploadReceiver implements CmsNames {
+
+               private Node context;
+               private Section section;
+               private String name;
+
+               public FormFileUploadReceiver(Section section, Node context, String name) {
+                       this.context = context;
+                       this.section = section;
+                       this.name = name;
+               }
+
+               @Override
+               public void receive(InputStream stream, FileDetails details) throws IOException {
+
+                       if (name == null)
+                               name = details.getFileName();
+
+                       // TODO clean image name more carefully
+                       String cleanedName = name.replaceAll("[^a-zA-Z0-9-.]", "");
+                       // We add a unique prefix to workaround the cache issue: when
+                       // deleting and re-adding a new image with same name, the end user
+                       // browser will use the cache and the image will remain unchanged
+                       // for a while
+                       cleanedName = System.currentTimeMillis() % 100000 + "_" + cleanedName;
+
+                       try {
+                               imageManager().uploadImage(context, cleanedName, stream);
+                               // TODO clean refresh strategy
+                               section.getDisplay().asyncExec(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               try {
+                                                       FormPageViewer.this.refresh(section);
+                                                       section.layout();
+                                                       section.getParent().layout();
+                                               } catch (RepositoryException re) {
+                                                       throw new CmsException("unable to refresh " + "image section for " + context);
+                                               }
+                                       }
+                               });
+                       } catch (RepositoryException re) {
+                               throw new CmsException("unable to upload image " + name + " at " + context);
+                       }
+               }
+       }
+
+       protected void addListeners(StyledControl control) {
+               control.setMouseListener(getMouseListener());
+               control.setFocusListener(getFocusListener());
+       }
+
+       protected Img createImgComposite(Composite parent, Node node, Point preferredSize) throws RepositoryException {
+               Img img = new Img(parent, SWT.NONE, node, preferredSize) {
+                       private static final long serialVersionUID = 1297900641952417540L;
+
+                       @Override
+                       protected void setContainerLayoutData(Composite composite) {
+                               composite.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
+                       }
+
+                       @Override
+                       protected void setControlLayoutData(Control control) {
+                               control.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
+                       }
+               };
+               img.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
+               updateContent(img);
+               addListeners(img);
+               return img;
+       }
+
+       protected Composite addDeleteAbility(final Section section, final Node sessionNode, int topWeight,
+                       int rightWeight) {
+               Composite comp = new Composite(section, SWT.NONE);
+               comp.setLayoutData(CmsUtils.fillAll());
+               comp.setLayout(new FormLayout());
+
+               // The body to be populated
+               Composite body = new Composite(comp, SWT.NO_FOCUS);
+               body.setLayoutData(EclipseUiUtils.fillFormData());
+
+               if (getCmsEditable().canEdit()) {
+                       // the delete button
+                       Button deleteBtn = new Button(comp, SWT.FLAT);
+                       CmsUtils.style(deleteBtn, FormStyle.deleteOverlay.style());
+                       FormData formData = new FormData();
+                       formData.right = new FormAttachment(rightWeight, 0);
+                       formData.top = new FormAttachment(topWeight, 0);
+                       deleteBtn.setLayoutData(formData);
+                       deleteBtn.moveAbove(body);
+
+                       deleteBtn.addSelectionListener(new SelectionAdapter() {
+                               private static final long serialVersionUID = 4304223543657238462L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       super.widgetSelected(e);
+                                       if (MessageDialog.openConfirm(section.getShell(), "Confirm deletion",
+                                                       "Are you really you want to remove this?")) {
+                                               Session session;
+                                               try {
+                                                       session = sessionNode.getSession();
+                                                       Section parSection = section.getParentSection();
+                                                       sessionNode.remove();
+                                                       session.save();
+                                                       refresh(parSection);
+                                                       layout(parSection);
+                                               } catch (RepositoryException re) {
+                                                       throw new CmsException("Unable to delete " + sessionNode, re);
+                                               }
+
+                                       }
+
+                               }
+                       });
+               }
+               return body;
+       }
+
+//     // LOCAL HELPERS FOR NODE MANAGEMENT
+//     private Node getOrCreateNode(Node parent, String nodeName, String nodeType) throws RepositoryException {
+//             Node node = null;
+//             if (getCmsEditable().canEdit() && !parent.hasNode(nodeName)) {
+//                     node = JcrUtils.mkdirs(parent, nodeName, nodeType);
+//                     parent.getSession().save();
+//             }
+//
+//             if (getCmsEditable().canEdit() || parent.hasNode(nodeName))
+//                     node = parent.getNode(nodeName);
+//
+//             return node;
+//     }
+
+       private SelectionListener getRemoveValueSelListener() {
+               return new SelectionAdapter() {
+                       private static final long serialVersionUID = 9022259089907445195L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               Object source = e.getSource();
+                               if (source instanceof Button) {
+                                       Button btn = (Button) source;
+                                       Object obj = btn.getData(FormConstants.LINKED_VALUE);
+                                       EditablePart ep = findDataParent(btn);
+                                       if (ep != null && ep instanceof EditableMultiStringProperty) {
+                                               EditableMultiStringProperty emsp = (EditableMultiStringProperty) ep;
+                                               List<String> values = emsp.getValues();
+                                               if (values.contains(obj)) {
+                                                       values.remove(values.indexOf(obj));
+                                                       emsp.setValues(values);
+                                                       try {
+                                                               save(emsp);
+                                                               // TODO workaround to force refresh
+                                                               edit(emsp, 0);
+                                                               cancelEdit();
+                                                       } catch (RepositoryException e1) {
+                                                               throw new CmsException("Unable to remove value " + obj, e1);
+                                                       }
+                                                       layout(emsp);
+                                               }
+                                       }
+                               }
+                       }
+               };
+       }
+
+       protected void setPropertySilently(Node node, String propName, String value) throws RepositoryException {
+               try {
+                       // TODO Clean this:
+                       // Format strings to replace \n
+                       value = value.replaceAll("\n", "<br/>");
+                       // Do not make the update if validation fails
+                       try {
+                               MarkupValidatorCopy.getInstance().validate(value);
+                       } catch (Exception e) {
+                               log.warn("Cannot set [" + value + "] on prop " + propName + "of " + node
+                                               + ", String cannot be validated - " + e.getMessage());
+                               return;
+                       }
+                       // TODO check if the newly created property is of the correct type,
+                       // otherwise the property will be silently created with a STRING
+                       // property type.
+                       node.setProperty(propName, value);
+               } catch (ValueFormatException vfe) {
+                       log.warn("Cannot set [" + value + "] on prop " + propName + "of " + node + " - " + vfe.getMessage());
+               }
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/forms/FormStyle.java b/org.argeo.cms.ui/src/org/argeo/cms/forms/FormStyle.java
new file mode 100644 (file)
index 0000000..6a496a2
--- /dev/null
@@ -0,0 +1,26 @@
+package org.argeo.cms.forms;
+
+/** Syles used */
+public enum FormStyle {
+       // Main
+       form, title,
+       // main part
+       header, headerBtn, headerCombo, section, sectionHeader,
+       // Property fields
+       propertyLabel, propertyText, propertyMessage, errorMessage,
+       // Date
+       popupCalendar,
+       // Buttons
+       starred, unstarred, starOverlay, editOverlay, deleteOverlay, updateOverlay, deleteOverlaySmall, calendar, delete,
+       // Contacts
+       email, address, phone, website,
+       // Social Media
+       facebook, twitter, linkedIn, instagram;
+
+       public String style() {
+               return form.name() + '_' + name();
+       }
+
+       // TODO clean button style management
+       public final static String BUTTON_SUFFIX = "_btn";
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/forms/FormUtils.java b/org.argeo.cms.ui/src/org/argeo/cms/forms/FormUtils.java
new file mode 100644 (file)
index 0000000..be08702
--- /dev/null
@@ -0,0 +1,197 @@
+package org.argeo.cms.forms;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.CmsView;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.fieldassist.ControlDecoration;
+import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** Utilitary methods to ease implementation of CMS forms */
+public class FormUtils {
+       private final static Log log = LogFactory.getLog(FormUtils.class);
+
+       public final static String DEFAULT_SHORT_DATE_FORMAT = "dd/MM/yyyy";
+
+       /** Best effort to convert a String to a calendar. Fails silently */
+       public static Calendar parseDate(DateFormat dateFormat, String calStr) {
+               Calendar cal = null;
+               if (EclipseUiUtils.notEmpty(calStr)) {
+                       try {
+                               Date date = dateFormat.parse(calStr);
+                               cal = new GregorianCalendar();
+                               cal.setTime(date);
+                       } catch (ParseException pe) {
+                               // Silent
+                               log.warn("Unable to parse date: " + calStr + " - msg: "
+                                               + pe.getMessage());
+                       }
+               }
+               return cal;
+       }
+
+       /** Add a double click listener on tables that display a JCR node list */
+       public static void addCanonicalDoubleClickListener(final TableViewer v) {
+               v.addDoubleClickListener(new IDoubleClickListener() {
+
+                       @Override
+                       public void doubleClick(DoubleClickEvent event) {
+                               CmsView cmsView = CmsUtils.getCmsView();
+                               Node node = (Node) ((IStructuredSelection) event.getSelection())
+                                               .getFirstElement();
+                               try {
+                                       cmsView.navigateTo(node.getPath());
+                               } catch (RepositoryException e) {
+                                       throw new CmsException("Unable to get path for node "
+                                                       + node + " before calling navigateTo(path)", e);
+                               }
+                       }
+               });
+       }
+
+       // MANAGE ERROR DECORATION
+
+       public static ControlDecoration addDecoration(final Text text) {
+               final ControlDecoration dynDecoration = new ControlDecoration(text,
+                               SWT.LEFT);
+               Image icon = getDecorationImage(FieldDecorationRegistry.DEC_ERROR);
+               dynDecoration.setImage(icon);
+               dynDecoration.setMarginWidth(3);
+               dynDecoration.hide();
+               return dynDecoration;
+       }
+
+       public static void refreshDecoration(Text text, ControlDecoration deco,
+                       boolean isValid, boolean clean) {
+               if (isValid || clean) {
+                       text.setBackground(null);
+                       deco.hide();
+               } else {
+                       text.setBackground(new Color(text.getDisplay(), 250, 200, 150));
+                       deco.show();
+               }
+       }
+
+       public static Image getDecorationImage(String image) {
+               FieldDecorationRegistry registry = FieldDecorationRegistry.getDefault();
+               return registry.getFieldDecoration(image).getImage();
+       }
+
+       public static void addCompulsoryDecoration(Label label) {
+               final ControlDecoration dynDecoration = new ControlDecoration(label,
+                               SWT.RIGHT | SWT.TOP);
+               Image icon = getDecorationImage(FieldDecorationRegistry.DEC_REQUIRED);
+               dynDecoration.setImage(icon);
+               dynDecoration.setMarginWidth(3);
+       }
+
+       // TODO the read only generation of read only links for various contact type
+       // should be factorised in the cms Utils.
+       /**
+        * Creates the read-only HTML snippet to display in a label with styling
+        * enabled in order to provide a click-able phone number
+        */
+       public static String getPhoneLink(String value) {
+               return getPhoneLink(value, value);
+       }
+
+       /**
+        * Creates the read-only HTML snippet to display in a label with styling
+        * enabled in order to provide a click-able phone number
+        * 
+        * @param value
+        * @param label
+        *            a potentially distinct label
+        * @return
+        */
+       public static String getPhoneLink(String value, String label) {
+               StringBuilder builder = new StringBuilder();
+               builder.append("<a href=\"tel:");
+               builder.append(value).append("\" target=\"_blank\" >").append(label)
+                               .append("</a>");
+               return builder.toString();
+       }
+
+       /**
+        * Creates the read-only HTML snippet to display in a label with styling
+        * enabled in order to provide a click-able mail
+        */
+       public static String getMailLink(String value) {
+               return getMailLink(value, value);
+       }
+
+       /**
+        * Creates the read-only HTML snippet to display in a label with styling
+        * enabled in order to provide a click-able mail
+        * 
+        * @param value
+        * @param label
+        *            a potentially distinct label
+        * @return
+        */
+       public static String getMailLink(String value, String label) {
+               StringBuilder builder = new StringBuilder();
+               value = replaceAmpersand(value);
+               builder.append("<a href=\"mailto:");
+               builder.append(value).append("\" >").append(label).append("</a>");
+               return builder.toString();
+       }
+
+       /**
+        * Creates the read-only HTML snippet to display in a label with styling
+        * enabled in order to provide a click-able link
+        */
+       public static String getUrlLink(String value) {
+               return getUrlLink(value, value);
+       }
+
+       /**
+        * Creates the read-only HTML snippet to display in a label with styling
+        * enabled in order to provide a click-able link
+        */
+       public static String getUrlLink(String value, String label) {
+               StringBuilder builder = new StringBuilder();
+               value = replaceAmpersand(value);
+               label = replaceAmpersand(label);
+               if (!(value.startsWith("http://") || value.startsWith("https://")))
+                       value = "http://" + value;
+               builder.append("<a href=\"");
+               builder.append(value + "\" target=\"_blank\" >" + label + "</a>");
+               return builder.toString();
+       }
+
+       private static String AMPERSAND = "&#38;";
+
+       /**
+        * Cleans a String by replacing any '&#38;' by its HTML encoding '&#38;#38;' to
+        * avoid <code>SAXParseException</code> while rendering HTML with RWT
+        */
+       public static String replaceAmpersand(String value) {
+               value = value.replaceAll("&(?![#a-zA-Z0-9]+;)", AMPERSAND);
+               return value;
+       }
+
+       // Prevents instantiation
+       private FormUtils() {
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/maintenance/AbstractOsgiComposite.java b/org.argeo.cms.ui/src/org/argeo/cms/maintenance/AbstractOsgiComposite.java
new file mode 100644 (file)
index 0000000..8c893f2
--- /dev/null
@@ -0,0 +1,42 @@
+package org.argeo.cms.maintenance;
+
+import java.util.Collection;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+
+abstract class AbstractOsgiComposite extends Composite {
+       private static final long serialVersionUID = -4097415973477517137L;
+       protected final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+       protected final Log log = LogFactory.getLog(getClass());
+
+       public AbstractOsgiComposite(Composite parent, int style) {
+               super(parent, style);
+               parent.setLayout(CmsUtils.noSpaceGridLayout());
+               setLayout(CmsUtils.noSpaceGridLayout());
+               setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               initUi(style);
+       }
+
+       protected abstract void initUi(int style);
+
+       protected <T> T getService(Class<? extends T> clazz) {
+               return bc.getService(bc.getServiceReference(clazz));
+       }
+
+       protected <T> Collection<ServiceReference<T>> getServiceReferences(Class<T> clazz, String filter) {
+               try {
+                       return bc.getServiceReferences(clazz, filter);
+               } catch (InvalidSyntaxException e) {
+                       throw new IllegalArgumentException("Filter " + filter + " is invalid", e);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/maintenance/Browse.java b/org.argeo.cms.ui/src/org/argeo/cms/maintenance/Browse.java
new file mode 100644 (file)
index 0000000..0389205
--- /dev/null
@@ -0,0 +1,616 @@
+package org.argeo.cms.maintenance;
+
+import static javax.jcr.Node.JCR_CONTENT;
+import static org.eclipse.swt.SWT.RIGHT;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.LinkedHashMap;
+
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.CmsTypes;
+import org.argeo.cms.text.Img;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.cms.util.CmsLink;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.widgets.EditableImage;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ILazyContentProvider;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Text;
+
+public class Browse implements CmsUiProvider {
+
+       // Some local constants to experiment. should be cleaned
+       private final static String BROWSE_PREFIX = "browse#";
+       private final static int THUMBNAIL_WIDTH = 400;
+       private final static int COLUMN_WIDTH = 160;
+       private DateFormat timeFormatter = new SimpleDateFormat(
+                       "dd-MM-yyyy', 'HH:mm");
+
+       // keep a cache of the opened nodes
+       // Key is the path
+       private LinkedHashMap<String, FilterEntitiesVirtualTable> browserCols = new LinkedHashMap<String, Browse.FilterEntitiesVirtualTable>();
+       private Composite nodeDisplayParent;
+       private Composite colViewer;
+       private ScrolledComposite scrolledCmp;
+       private Text parentPathTxt;
+       private Text filterTxt;
+       private Node currEdited;
+
+       private String initialPath;
+
+       @Override
+       public Control createUi(Composite parent, Node context)
+                       throws RepositoryException {
+               if (context == null)
+                       // return null;
+                       throw new CmsException("Context cannot be null");
+               GridLayout layout = CmsUtils.noSpaceGridLayout();
+               layout.numColumns = 2;
+               parent.setLayout(layout);
+
+               // Left
+               Composite leftCmp = new Composite(parent, SWT.NO_FOCUS);
+               leftCmp.setLayoutData(CmsUtils.fillAll());
+               createBrowserPart(leftCmp, context);
+
+               // Right
+               nodeDisplayParent = new Composite(parent, SWT.NO_FOCUS | SWT.BORDER);
+               GridData gd = new GridData(SWT.RIGHT, SWT.FILL, false, true);
+               gd.widthHint = THUMBNAIL_WIDTH;
+               nodeDisplayParent.setLayoutData(gd);
+               createNodeView(nodeDisplayParent, context);
+
+               // INIT
+               setEdited(context);
+               initialPath = context.getPath();
+
+               // Workaround we don't yet manage the delete to display parent of the
+               // initial context node
+
+               return null;
+       }
+
+       private void createBrowserPart(Composite parent, Node context)
+                       throws RepositoryException {
+               GridLayout layout = CmsUtils.noSpaceGridLayout();
+               parent.setLayout(layout);
+               Composite filterCmp = new Composite(parent, SWT.NO_FOCUS);
+               filterCmp.setLayoutData(CmsUtils.fillWidth());
+
+               // top filter
+               addFilterPanel(filterCmp);
+
+               // scrolled composite
+               scrolledCmp = new ScrolledComposite(parent, SWT.H_SCROLL | SWT.BORDER
+                               | SWT.NO_FOCUS);
+               scrolledCmp.setLayoutData(CmsUtils.fillAll());
+               scrolledCmp.setExpandVertical(true);
+               scrolledCmp.setExpandHorizontal(true);
+               scrolledCmp.setShowFocusedControl(true);
+
+               colViewer = new Composite(scrolledCmp, SWT.NO_FOCUS);
+               scrolledCmp.setContent(colViewer);
+               scrolledCmp.addControlListener(new ControlAdapter() {
+                       private static final long serialVersionUID = 6589392045145698201L;
+
+                       @Override
+                       public void controlResized(ControlEvent e) {
+                               Rectangle r = scrolledCmp.getClientArea();
+                               scrolledCmp.setMinSize(colViewer.computeSize(SWT.DEFAULT,
+                                               r.height));
+                       }
+               });
+               initExplorer(colViewer, context);
+       }
+
+       private Control initExplorer(Composite parent, Node context)
+                       throws RepositoryException {
+               parent.setLayout(CmsUtils.noSpaceGridLayout());
+               createBrowserColumn(parent, context);
+               return null;
+       }
+
+       private Control createBrowserColumn(Composite parent, Node context)
+                       throws RepositoryException {
+               // TODO style is not correctly managed.
+               FilterEntitiesVirtualTable table = new FilterEntitiesVirtualTable(
+                               parent, SWT.BORDER | SWT.NO_FOCUS, context);
+               // CmsUtils.style(table, ArgeoOrgStyle.browserColumn.style());
+               table.filterList("*");
+               table.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, false, true));
+               browserCols.put(context.getPath(), table);
+               return null;
+       }
+
+       public void addFilterPanel(Composite parent) {
+
+               parent.setLayout(CmsUtils.noSpaceGridLayout(new GridLayout(2, false)));
+
+               // Text Area for the filter
+               parentPathTxt = new Text(parent, SWT.NO_FOCUS);
+               parentPathTxt.setEditable(false);
+               filterTxt = new Text(parent, SWT.SEARCH | SWT.ICON_CANCEL);
+               filterTxt.setMessage("Filter current list");
+               filterTxt.setLayoutData(CmsUtils.fillWidth());
+               filterTxt.addModifyListener(new ModifyListener() {
+                       private static final long serialVersionUID = 7709303319740056286L;
+
+                       public void modifyText(ModifyEvent event) {
+                               modifyFilter(false);
+                       }
+               });
+
+               filterTxt.addKeyListener(new KeyListener() {
+                       private static final long serialVersionUID = -4523394262771183968L;
+
+                       @Override
+                       public void keyReleased(KeyEvent e) {
+                       }
+
+                       @Override
+                       public void keyPressed(KeyEvent e) {
+                               boolean shiftPressed = (e.stateMask & SWT.SHIFT) != 0;
+                               // boolean altPressed = (e.stateMask & SWT.ALT) != 0;
+                               FilterEntitiesVirtualTable currTable = null;
+                               if (currEdited != null) {
+                                       FilterEntitiesVirtualTable table = browserCols
+                                                       .get(getPath(currEdited));
+                                       if (table != null && !table.isDisposed())
+                                               currTable = table;
+                               }
+
+                               try {
+                                       if (e.keyCode == SWT.ARROW_DOWN)
+                                               currTable.setFocus();
+                                       else if (e.keyCode == SWT.BS) {
+                                               if (filterTxt.getText().equals("")
+                                                               && !(getPath(currEdited).equals("/") || getPath(
+                                                                               currEdited).equals(initialPath))) {
+                                                       setEdited(currEdited.getParent());
+                                                       e.doit = false;
+                                                       filterTxt.setFocus();
+                                               }
+                                       } else if (e.keyCode == SWT.TAB && !shiftPressed) {
+                                               if (currEdited.getNodes(filterTxt.getText() + "*")
+                                                               .getSize() == 1) {
+                                                       setEdited(currEdited.getNodes(
+                                                                       filterTxt.getText() + "*").nextNode());
+                                               }
+                                               filterTxt.setFocus();
+                                               e.doit = false;
+                                       }
+                               } catch (RepositoryException e1) {
+                                       throw new CmsException(
+                                                       "Unexpected error in key management for "
+                                                                       + currEdited + "with filter "
+                                                                       + filterTxt.getText(), e1);
+                               }
+
+                       }
+               });
+       }
+
+       private void setEdited(Node node) {
+               try {
+                       currEdited = node;
+                       CmsUtils.clear(nodeDisplayParent);
+                       createNodeView(nodeDisplayParent, currEdited);
+                       nodeDisplayParent.layout();
+                       refreshFilters(node);
+                       refreshBrowser(node);
+               } catch (RepositoryException re) {
+                       throw new CmsException("Unable to update browser for " + node, re);
+               }
+       }
+
+       private void refreshFilters(Node node) throws RepositoryException {
+               String currNodePath = node.getPath();
+               parentPathTxt.setText(currNodePath);
+               filterTxt.setText("");
+               filterTxt.getParent().layout();
+       }
+
+       private void refreshBrowser(Node node) throws RepositoryException {
+
+               // Retrieve
+               String currNodePath = node.getPath();
+               String currParPath = "";
+               if (!"/".equals(currNodePath))
+                       currParPath = JcrUtils.parentPath(currNodePath);
+               if ("".equals(currParPath))
+                       currParPath = "/";
+               
+               
+               
+
+               Object[][] colMatrix = new Object[browserCols.size()][2];
+
+               int i = 0, j = -1, k = -1;
+               for (String path : browserCols.keySet()) {
+                       colMatrix[i][0] = path;
+                       colMatrix[i][1] = browserCols.get(path);
+                       if (j >= 0 && k < 0 && !currNodePath.equals("/")) {
+                               boolean leaveOpened = path.startsWith(currNodePath);
+
+                               // workaround for same name siblings
+                               // fix me weird side effect when we go left or click on anb
+                               // already selected, unfocused node
+                               if (leaveOpened
+                                               && (path.lastIndexOf("/") == 0
+                                                               && currNodePath.lastIndexOf("/") == 0 || JcrUtils
+                                                               .parentPath(path).equals(
+                                                                               JcrUtils.parentPath(currNodePath))))
+                                       leaveOpened = JcrUtils.lastPathElement(path).equals(
+                                                       JcrUtils.lastPathElement(currNodePath));
+
+                               if (!leaveOpened)
+                                       k = i;
+                       }
+                       if (currParPath.equals(path))
+                               j = i;
+                       i++;
+               }
+
+               if (j >= 0 && k >= 0)
+                       // remove useless cols
+                       for (int l = i - 1; l >= k; l--) {
+                               browserCols.remove(colMatrix[l][0]);
+                               ((FilterEntitiesVirtualTable) colMatrix[l][1]).dispose();
+                       }
+
+               // Remove disposed columns
+               // TODO investigate and fix the mechanism that leave them there after
+               // disposal
+               if (browserCols.containsKey(currNodePath)) {
+                       FilterEntitiesVirtualTable currCol = browserCols.get(currNodePath);
+                       if (currCol.isDisposed())
+                               browserCols.remove(currNodePath);
+               }
+
+               if (!browserCols.containsKey(currNodePath))
+                       createBrowserColumn(colViewer, node);
+
+               colViewer.setLayout(CmsUtils.noSpaceGridLayout(new GridLayout(
+                               browserCols.size(), false)));
+               // colViewer.pack();
+               colViewer.layout();
+               // also resize the scrolled composite
+               scrolledCmp.layout();
+               scrolledCmp.getShowFocusedControl();
+               // colViewer.getParent().layout();
+               // if (JcrUtils.parentPath(currNodePath).equals(currBrowserKey)) {
+               // } else {
+               // }
+       }
+
+       private void modifyFilter(boolean fromOutside) {
+               if (!fromOutside)
+                       if (currEdited != null) {
+                               String filter = filterTxt.getText() + "*";
+                               FilterEntitiesVirtualTable table = browserCols
+                                               .get(getPath(currEdited));
+                               if (table != null && !table.isDisposed())
+                                       table.filterList(filter);
+                       }
+
+       }
+
+       private String getPath(Node node) {
+               try {
+                       return node.getPath();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Unable to get path for node " + node, e);
+               }
+       }
+
+       private Point imageWidth = new Point(250, 0);
+
+       /**
+        * Recreates the content of the box that displays information about the
+        * current selected node.
+        */
+       private Control createNodeView(Composite parent, Node context)
+                       throws RepositoryException {
+
+               parent.setLayout(new GridLayout(2, false));
+
+               if (isImg(context)) {
+                       EditableImage image = new Img(parent, RIGHT, context, imageWidth);
+                       image.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true,
+                                       false, 2, 1));
+               }
+
+               // Name and primary type
+               Label contextL = new Label(parent, SWT.NONE);
+               contextL.setData(RWT.MARKUP_ENABLED, true);
+               contextL.setText("<b>" + context.getName() + "</b>");
+               new Label(parent, SWT.NONE).setText(context.getPrimaryNodeType()
+                               .getName());
+
+               // Children
+               for (NodeIterator nIt = context.getNodes(); nIt.hasNext();) {
+                       Node child = nIt.nextNode();
+                       new CmsLink(child.getName(), BROWSE_PREFIX + child.getPath())
+                                       .createUi(parent, context);
+                       new Label(parent, SWT.NONE).setText(child.getPrimaryNodeType()
+                                       .getName());
+               }
+
+               // Properties
+               for (PropertyIterator pIt = context.getProperties(); pIt.hasNext();) {
+                       Property property = pIt.nextProperty();
+                       Label label = new Label(parent, SWT.NONE);
+                       label.setText(property.getName());
+                       label.setToolTipText(JcrUtils
+                                       .getPropertyDefinitionAsString(property));
+                       new Label(parent, SWT.NONE).setText(getPropAsString(property));
+               }
+
+               return null;
+       }
+
+       private boolean isImg(Node node) throws RepositoryException {
+               return node.hasNode(JCR_CONTENT) && node.isNodeType(CmsTypes.CMS_IMAGE);
+       }
+
+       private String getPropAsString(Property property)
+                       throws RepositoryException {
+               String result = "";
+               if (property.isMultiple()) {
+                       result = getMultiAsString(property, ", ");
+               } else {
+                       Value value = property.getValue();
+                       if (value.getType() == PropertyType.BINARY)
+                               result = "<binary>";
+                       else if (value.getType() == PropertyType.DATE)
+                               result = timeFormatter.format(value.getDate().getTime());
+                       else
+                               result = value.getString();
+               }
+               return result;
+       }
+
+       private String getMultiAsString(Property property, String separator)
+                       throws RepositoryException {
+               if (separator == null)
+                       separator = "; ";
+               Value[] values = property.getValues();
+               StringBuilder builder = new StringBuilder();
+               for (Value val : values) {
+                       String currStr = val.getString();
+                       if (!"".equals(currStr.trim()))
+                               builder.append(currStr).append(separator);
+               }
+               if (builder.lastIndexOf(separator) >= 0)
+                       return builder.substring(0, builder.length() - separator.length());
+               else
+                       return builder.toString();
+       }
+
+       /** Almost canonical implementation of a table that display entities */
+       private class FilterEntitiesVirtualTable extends Composite {
+               private static final long serialVersionUID = 8798147431706283824L;
+
+               // Context
+               private Node context;
+
+               // UI Objects
+               private TableViewer entityViewer;
+
+               // enable management of multiple columns
+               Node getNode() {
+                       return context;
+               }
+
+               @Override
+               public boolean setFocus() {
+                       if (entityViewer.getTable().isDisposed())
+                               return false;
+                       if (entityViewer.getSelection().isEmpty()) {
+                               Object first = entityViewer.getElementAt(0);
+                               if (first != null) {
+                                       entityViewer.setSelection(new StructuredSelection(first),
+                                                       true);
+                               }
+                       }
+                       return entityViewer.getTable().setFocus();
+               }
+
+               void filterList(String filter) {
+                       try {
+                               NodeIterator nit = context.getNodes(filter);
+                               refreshFilteredList(nit);
+                       } catch (RepositoryException e) {
+                               throw new CmsException("Unable to filter " + getNode()
+                                               + " children with filter " + filter, e);
+                       }
+
+               }
+
+               public FilterEntitiesVirtualTable(Composite parent, int style,
+                               Node context) {
+                       super(parent, SWT.NO_FOCUS);
+                       this.context = context;
+                       populate();
+               }
+
+               protected void populate() {
+                       Composite parent = this;
+                       GridLayout layout = CmsUtils.noSpaceGridLayout();
+
+                       this.setLayout(layout);
+                       createTableViewer(parent);
+               }
+
+               private void createTableViewer(final Composite parent) {
+                       // the list
+                       // We must limit the size of the table otherwise the full list is
+                       // loaded
+                       // before the layout happens
+                       Composite listCmp = new Composite(parent, SWT.NO_FOCUS);
+                       GridData gd = new GridData(SWT.LEFT, SWT.FILL, false, true);
+                       gd.widthHint = COLUMN_WIDTH;
+                       listCmp.setLayoutData(gd);
+                       listCmp.setLayout(CmsUtils.noSpaceGridLayout());
+
+                       entityViewer = new TableViewer(listCmp, SWT.VIRTUAL | SWT.SINGLE);
+                       Table table = entityViewer.getTable();
+
+                       table.setLayoutData(CmsUtils.fillAll());
+                       table.setLinesVisible(true);
+                       table.setHeaderVisible(false);
+                       table.setData(RWT.MARKUP_ENABLED, Boolean.TRUE);
+
+                       CmsUtils.style(table, MaintenanceStyles.BROWSER_COLUMN);
+
+                       // first column
+                       TableViewerColumn column = new TableViewerColumn(entityViewer,
+                                       SWT.NONE);
+                       TableColumn tcol = column.getColumn();
+                       tcol.setWidth(COLUMN_WIDTH);
+                       tcol.setResizable(true);
+                       column.setLabelProvider(new SimpleNameLP());
+
+                       entityViewer.setContentProvider(new MyLazyCP(entityViewer));
+                       entityViewer
+                                       .addSelectionChangedListener(new ISelectionChangedListener() {
+
+                                               @Override
+                                               public void selectionChanged(SelectionChangedEvent event) {
+                                                       IStructuredSelection selection = (IStructuredSelection) entityViewer
+                                                                       .getSelection();
+                                                       if (selection.isEmpty())
+                                                               return;
+                                                       else
+                                                               setEdited((Node) selection.getFirstElement());
+
+                                               }
+                                       });
+
+                       table.addKeyListener(new KeyListener() {
+                               private static final long serialVersionUID = -330694313896036230L;
+
+                               @Override
+                               public void keyReleased(KeyEvent e) {
+                               }
+
+                               @Override
+                               public void keyPressed(KeyEvent e) {
+
+                                       IStructuredSelection selection = (IStructuredSelection) entityViewer
+                                                       .getSelection();
+                                       Node selected = null;
+                                       if (!selection.isEmpty())
+                                               selected = ((Node) selection.getFirstElement());
+                                       try {
+                                               if (e.keyCode == SWT.ARROW_RIGHT) {
+                                                       if (selected != null) {
+                                                               setEdited(selected);
+                                                               browserCols.get(selected.getPath()).setFocus();
+                                                       }
+                                               } else if (e.keyCode == SWT.ARROW_LEFT) {
+                                                       try {
+                                                               selected = getNode().getParent();
+                                                               String newPath = selected.getPath(); // getNode().getParent()
+                                                               setEdited(selected);
+                                                               if (browserCols.containsKey(newPath))
+                                                                       browserCols.get(newPath).setFocus();
+                                                       } catch (ItemNotFoundException ie) {
+                                                               // root silent
+                                                       }
+                                               }
+                                       } catch (RepositoryException ie) {
+                                               throw new CmsException("Error while managing arrow "
+                                                               + "events in the browser for " + selected, ie);
+                                       }
+                               }
+                       });
+               }
+
+               private class MyLazyCP implements ILazyContentProvider {
+                       private static final long serialVersionUID = 1L;
+                       private TableViewer viewer;
+                       private Object[] elements;
+
+                       public MyLazyCP(TableViewer viewer) {
+                               this.viewer = viewer;
+                       }
+
+                       public void dispose() {
+                       }
+
+                       public void inputChanged(Viewer viewer, Object oldInput,
+                                       Object newInput) {
+                               // IMPORTANT: don't forget this: an exception will be thrown if
+                               // a selected object is not part of the results anymore.
+                               viewer.setSelection(null);
+                               this.elements = (Object[]) newInput;
+                       }
+
+                       public void updateElement(int index) {
+                               viewer.replace(elements[index], index);
+                       }
+               }
+
+               protected void refreshFilteredList(NodeIterator children) {
+                       Object[] rows = JcrUtils.nodeIteratorToList(children).toArray();
+                       entityViewer.setInput(rows);
+                       entityViewer.setItemCount(rows.length);
+                       entityViewer.refresh();
+               }
+
+               public class SimpleNameLP extends ColumnLabelProvider {
+                       private static final long serialVersionUID = 2465059387875338553L;
+
+                       @Override
+                       public String getText(Object element) {
+                               if (element instanceof Node) {
+                                       Node curr = ((Node) element);
+                                       try {
+                                               return curr.getName();
+                                       } catch (RepositoryException e) {
+                                               throw new CmsException("Unable to get name for"
+                                                               + curr);
+                                       }
+                               }
+                               return super.getText(element);
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/maintenance/ConnectivityDeploymentUi.java b/org.argeo.cms.ui/src/org/argeo/cms/maintenance/ConnectivityDeploymentUi.java
new file mode 100644 (file)
index 0000000..f4f3079
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.cms.maintenance;
+
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.useradmin.UserAdmin;
+
+class ConnectivityDeploymentUi extends AbstractOsgiComposite {
+       private static final long serialVersionUID = 590221539553514693L;
+
+       public ConnectivityDeploymentUi(Composite parent, int style) {
+               super(parent, style);
+       }
+
+       @Override
+       protected void initUi(int style) {
+               StringBuffer text = new StringBuffer();
+               text.append("<span style='font-variant: small-caps;'>Provided Servers</span><br/>");
+
+               ServiceReference<HttpService> userAdminRef = bc.getServiceReference(HttpService.class);
+               if (userAdminRef != null) {
+                       // FIXME use constants
+                       Object httpPort = userAdminRef.getProperty("http.port");
+                       Object httpsPort = userAdminRef.getProperty("https.port");
+                       if (httpPort != null)
+                               text.append("<b>http</b> ").append(httpPort).append("<br/>");
+                       if (httpsPort != null)
+                               text.append("<b>https</b> ").append(httpsPort).append("<br/>");
+
+               }
+
+               text.append("<br/>");
+               text.append("<span style='font-variant: small-caps;'>Referenced Servers</span><br/>");
+
+               Label label = new Label(this, SWT.NONE);
+               label.setData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               CmsUtils.markup(label);
+               label.setText(text.toString());
+       }
+
+       protected boolean isDeployed() {
+               return bc.getServiceReference(UserAdmin.class) != null;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/maintenance/DataDeploymentUi.java b/org.argeo.cms.ui/src/org/argeo/cms/maintenance/DataDeploymentUi.java
new file mode 100644 (file)
index 0000000..613e3cb
--- /dev/null
@@ -0,0 +1,139 @@
+package org.argeo.cms.maintenance;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collection;
+
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.node.NodeConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.osgi.framework.ServiceReference;
+
+class DataDeploymentUi extends AbstractOsgiComposite {
+       private static final long serialVersionUID = 590221539553514693L;
+
+       public DataDeploymentUi(Composite parent, int style) {
+               super(parent, style);
+       }
+
+       @Override
+       protected void initUi(int style) {
+               if (isDeployed()) {
+                       initCurrentUi(this);
+               } else {
+                       initNewUi(this);
+               }
+       }
+
+       private void initNewUi(Composite parent) {
+//             try {
+//                     ConfigurationAdmin confAdmin = bc.getService(bc.getServiceReference(ConfigurationAdmin.class));
+//                     Configuration[] confs = confAdmin.listConfigurations(
+//                                     "(" + ConfigurationAdmin.SERVICE_FACTORYPID + "=" + NodeConstants.NODE_REPOS_FACTORY_PID + ")");
+//                     if (confs == null || confs.length == 0) {
+//                             Group buttonGroup = new Group(parent, SWT.NONE);
+//                             buttonGroup.setText("Repository Type");
+//                             buttonGroup.setLayout(new GridLayout(2, true));
+//                             buttonGroup.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+//
+//                             SelectionListener selectionListener = new SelectionAdapter() {
+//                                     private static final long serialVersionUID = 6247064348421088092L;
+//
+//                                     public void widgetSelected(SelectionEvent event) {
+//                                             Button radio = (Button) event.widget;
+//                                             if (!radio.getSelection())
+//                                                     return;
+//                                             log.debug(event);
+//                                             JackrabbitType nodeType = (JackrabbitType) radio.getData();
+//                                             if (log.isDebugEnabled())
+//                                                     log.debug(" selected = " + nodeType.name());
+//                                     };
+//                             };
+//
+//                             for (JackrabbitType nodeType : JackrabbitType.values()) {
+//                                     Button radio = new Button(buttonGroup, SWT.RADIO);
+//                                     radio.setText(nodeType.name());
+//                                     radio.setData(nodeType);
+//                                     if (nodeType.equals(JackrabbitType.localfs))
+//                                             radio.setSelection(true);
+//                                     radio.addSelectionListener(selectionListener);
+//                             }
+//
+//                     } else if (confs.length == 1) {
+//
+//                     } else {
+//                             throw new CmsException("Multiple repos not yet supported");
+//                     }
+//             } catch (Exception e) {
+//                     throw new CmsException("Cannot initialize UI", e);
+//             }
+
+       }
+
+       private void initCurrentUi(Composite parent) {
+               parent.setLayout(new GridLayout());
+               Collection<ServiceReference<RepositoryContext>> contexts = getServiceReferences(RepositoryContext.class,
+                               "(" + NodeConstants.CN + "=*)");
+               StringBuffer text = new StringBuffer();
+               text.append("<span style='font-variant: small-caps;'>Jackrabbit Repositories</span><br/>");
+               for (ServiceReference<RepositoryContext> sr : contexts) {
+                       RepositoryContext repositoryContext = bc.getService(sr);
+                       String alias = sr.getProperty(NodeConstants.CN).toString();
+                       String rootNodeId = repositoryContext.getRootNodeId().toString();
+                       RepositoryConfig repositoryConfig = repositoryContext.getRepositoryConfig();
+                       Path repoHomePath = new File(repositoryConfig.getHomeDir()).toPath().toAbsolutePath();
+                       // TODO check data store
+
+                       text.append("<b>" + alias + "</b><br/>");
+                       text.append("rootNodeId: " + rootNodeId + "<br/>");
+                       try {
+                               FileStore fileStore = Files.getFileStore(repoHomePath);
+                               text.append("partition: " + fileStore.toString() + "<br/>");
+                               text.append(
+                                               percentUsed(fileStore) + " used (" + humanReadable(fileStore.getUsableSpace()) + " free)<br/>");
+                       } catch (IOException e) {
+                               log.error("Cannot check fileStore for " + repoHomePath, e);
+                       }
+               }
+               Label label = new Label(parent, SWT.NONE);
+               label.setData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               CmsUtils.markup(label);
+               label.setText("<span style=''>" + text.toString() + "</span>");
+       }
+
+       private String humanReadable(long bytes) {
+               long mb = bytes / (1024 * 1024);
+               return mb >= 2048 ? Long.toString(mb / 1024) + " GB" : Long.toString(mb) + " MB";
+       }
+
+       private String percentUsed(FileStore fs) throws IOException {
+               long used = fs.getTotalSpace() - fs.getUnallocatedSpace();
+               long percent = used * 100 / fs.getTotalSpace();
+               if (log.isTraceEnabled()) {
+                       // output identical to `df -B 1`)
+                       log.trace(fs.getTotalSpace() + "," + used + "," + fs.getUsableSpace());
+               }
+               String span;
+               if (percent < 80)
+                       span = "<span style='color:green;font-weight:bold'>";
+               else if (percent < 95)
+                       span = "<span style='color:orange;font-weight:bold'>";
+               else
+                       span = "<span style='color:red;font-weight:bold'>";
+               return span + percent + "%</span>";
+       }
+
+       protected boolean isDeployed() {
+               return bc.getServiceReference(RepositoryContext.class) != null;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/maintenance/DeploymentEntryPoint.java b/org.argeo.cms.ui/src/org/argeo/cms/maintenance/DeploymentEntryPoint.java
new file mode 100644 (file)
index 0000000..8dda4c4
--- /dev/null
@@ -0,0 +1,98 @@
+package org.argeo.cms.maintenance;
+
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeDeployment;
+import org.argeo.node.NodeState;
+import org.eclipse.rap.rwt.application.AbstractEntryPoint;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+
+class DeploymentEntryPoint extends AbstractEntryPoint {
+       private static final long serialVersionUID = -881152502968982437L;
+       private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+       @Override
+       protected void createContents(Composite parent) {
+               // FIXME manage authentication if needed
+               // if (!CurrentUser.roles().contains(AuthConstants.ROLE_ADMIN))
+               // return;
+
+               // parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               if (isDesktop()) {
+                       parent.setLayout(new GridLayout(2, true));
+               } else {
+                       // TODO add scrolling
+                       parent.setLayout(new GridLayout(1, true));
+               }
+
+               initHighLevelSummary(parent);
+
+               Group securityGroup = createHighLevelGroup(parent, "Security");
+               securityGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               new SecurityDeploymentUi(securityGroup, SWT.NONE);
+
+               Group dataGroup = createHighLevelGroup(parent, "Data");
+               dataGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               new DataDeploymentUi(dataGroup, SWT.NONE);
+
+               Group logGroup = createHighLevelGroup(parent, "Notifications");
+               logGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true));
+               new LogDeploymentUi(logGroup, SWT.NONE);
+
+               Group connectivityGroup = createHighLevelGroup(parent, "Connectivity");
+               new ConnectivityDeploymentUi(connectivityGroup, SWT.NONE);
+               connectivityGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, true));
+
+       }
+
+       private void initHighLevelSummary(Composite parent) {
+               Composite composite = new Composite(parent, SWT.NONE);
+               GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false);
+               if (isDesktop())
+                       gridData.horizontalSpan = 3;
+               composite.setLayoutData(gridData);
+               composite.setLayout(new FillLayout());
+
+               ServiceReference<NodeState> nodeStateRef = bc.getServiceReference(NodeState.class);
+               if (nodeStateRef == null)
+                       throw new IllegalStateException("No CMS state available");
+               NodeState nodeState = bc.getService(nodeStateRef);
+               ServiceReference<NodeDeployment> nodeDeploymentRef = bc.getServiceReference(NodeDeployment.class);
+               Label label = new Label(composite, SWT.WRAP);
+               CmsUtils.markup(label);
+               if (nodeDeploymentRef == null) {
+                       label.setText("Not yet deployed on <br>" + nodeState.getHostname() + "</br>, please configure below.");
+               } else {
+                       Object stateUuid = nodeStateRef.getProperty(NodeConstants.CN);
+                       NodeDeployment nodeDeployment = bc.getService(nodeDeploymentRef);
+                       GregorianCalendar calendar = new GregorianCalendar();
+                       calendar.setTimeInMillis(nodeDeployment.getAvailableSince());
+                       calendar.setTimeZone(TimeZone.getDefault());
+                       label.setText("[" + "<b>" + nodeState.getHostname() + "</b>]# " + "Deployment state " + stateUuid
+                                       + ", available since <b>" + calendar.getTime() + "</b>");
+               }
+       }
+
+       private static Group createHighLevelGroup(Composite parent, String text) {
+               Group group = new Group(parent, SWT.NONE);
+               group.setText(text);
+               CmsUtils.markup(group);
+               return group;
+       }
+
+       private boolean isDesktop() {
+               return true;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/maintenance/LogDeploymentUi.java b/org.argeo.cms.ui/src/org/argeo/cms/maintenance/LogDeploymentUi.java
new file mode 100644 (file)
index 0000000..8fb9643
--- /dev/null
@@ -0,0 +1,74 @@
+package org.argeo.cms.maintenance;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Enumeration;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogListener;
+import org.osgi.service.log.LogReaderService;
+
+class LogDeploymentUi extends AbstractOsgiComposite implements LogListener {
+       private static final long serialVersionUID = 590221539553514693L;
+
+       private DateFormat dateFormat = new SimpleDateFormat("MMdd HH:mm");
+
+       private Display display;
+       private Text logDisplay;
+
+       public LogDeploymentUi(Composite parent, int style) {
+               super(parent, style);
+       }
+
+       @Override
+       protected void initUi(int style) {
+               LogReaderService logReader = getService(LogReaderService.class);
+               // FIXME use server push
+               // logReader.addLogListener(this);
+               this.display = getDisplay();
+               this.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               logDisplay = new Text(this, SWT.WRAP | SWT.MULTI | SWT.READ_ONLY);
+               logDisplay.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               CmsUtils.markup(logDisplay);
+               @SuppressWarnings("unchecked")
+               Enumeration<LogEntry> logEntries = (Enumeration<LogEntry>) logReader.getLog();
+               while (logEntries.hasMoreElements())
+                       logDisplay.append(printEntry(logEntries.nextElement()));
+       }
+
+       private String printEntry(LogEntry entry) {
+               StringBuilder sb = new StringBuilder();
+               GregorianCalendar calendar = new GregorianCalendar(TimeZone.getDefault());
+               calendar.setTimeInMillis(entry.getTime());
+               sb.append(dateFormat.format(calendar.getTime())).append(' ');
+               sb.append(entry.getMessage());
+               sb.append('\n');
+               return sb.toString();
+       }
+
+       @Override
+       public void logged(LogEntry entry) {
+               if (display.isDisposed())
+                       return;
+               display.asyncExec(() -> {
+                       if (logDisplay.isDisposed())
+                               return;
+                       logDisplay.append(printEntry(entry));
+               });
+               display.wake();
+       }
+
+       // @Override
+       // public void dispose() {
+       // super.dispose();
+       // getService(LogReaderService.class).removeLogListener(this);
+       // }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/maintenance/MaintenanceStyles.java b/org.argeo.cms.ui/src/org/argeo/cms/maintenance/MaintenanceStyles.java
new file mode 100644 (file)
index 0000000..fef25d7
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.cms.maintenance;
+
+/** Specific styles used by the various maintenance pages . */
+public interface MaintenanceStyles {
+       // General
+       public final static String PREFIX = "maintenance_";
+
+       // Browser
+       public final static String BROWSER_COLUMN = "browser_column";
+       }
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/maintenance/MaintenanceUi.java b/org.argeo.cms.ui/src/org/argeo/cms/maintenance/MaintenanceUi.java
new file mode 100644 (file)
index 0000000..11b0b90
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.cms.maintenance;
+
+import org.eclipse.rap.rwt.application.Application;
+import org.eclipse.rap.rwt.application.ApplicationConfiguration;
+
+public class MaintenanceUi implements ApplicationConfiguration {
+
+       @Override
+       public void configure(Application application) {
+               // application.addEntryPoint("/status", DeploymentEntryPoint.class,
+               // null);
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/maintenance/NonAdminPage.java b/org.argeo.cms.ui/src/org/argeo/cms/maintenance/NonAdminPage.java
new file mode 100644 (file)
index 0000000..8a90344
--- /dev/null
@@ -0,0 +1,30 @@
+package org.argeo.cms.maintenance;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+
+public class NonAdminPage implements CmsUiProvider{
+
+       @Override
+       public Control createUi(Composite parent, Node context)
+                       throws RepositoryException {
+               Composite body = new Composite(parent, SWT.NO_FOCUS);
+               body.setLayoutData(CmsUtils.fillAll());
+               body.setLayout(new GridLayout());
+               Label label = new Label(body, SWT.NONE);
+               label.setText("You should be an admin to perform maintenance operations. "
+                               + "Are you sure you are logged in?");
+               label.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
+               return null;
+       }
+       
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/maintenance/SecurityDeploymentUi.java b/org.argeo.cms.ui/src/org/argeo/cms/maintenance/SecurityDeploymentUi.java
new file mode 100644 (file)
index 0000000..9fcdaf9
--- /dev/null
@@ -0,0 +1,85 @@
+package org.argeo.cms.maintenance;
+
+import java.net.URI;
+
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdmin;
+
+class SecurityDeploymentUi extends AbstractOsgiComposite {
+       private static final long serialVersionUID = 590221539553514693L;
+
+       public SecurityDeploymentUi(Composite parent, int style) {
+               super(parent, style);
+       }
+
+       @Override
+       protected void initUi(int style) {
+               if (isDeployed()) {
+                       initCurrentUi(this);
+               } else {
+                       initNewUi(this);
+               }
+       }
+
+       private void initNewUi(Composite parent) {
+               new Label(parent, SWT.NONE).setText("Security is not configured");
+       }
+
+       private void initCurrentUi(Composite parent) {
+               ServiceReference<UserAdmin> userAdminRef = bc.getServiceReference(UserAdmin.class);
+               UserAdmin userAdmin = bc.getService(userAdminRef);
+               StringBuffer text = new StringBuffer();
+               text.append("<span style='font-variant: small-caps;'>Domains</span><br/>");
+               domains: for (String key : userAdminRef.getPropertyKeys()) {
+                       if (!key.startsWith("/"))
+                               continue domains;
+                       URI uri;
+                       try {
+                               uri = new URI(key);
+                       } catch (Exception e) {
+                               // ignore non URI keys
+                               continue domains;
+                       }
+
+                       String rootDn = uri.getPath().substring(1, uri.getPath().length());
+                       // FIXME make reading query options more robust, using utils
+                       boolean readOnly = uri.getQuery().equals("readOnly=true");
+                       if (readOnly)
+                               text.append("<span style='font-weight:bold;font-style: italic'>");
+                       else
+                               text.append("<span style='font-weight:bold'>");
+
+                       text.append(rootDn);
+                       text.append("</span><br/>");
+                       try {
+                               Role[] roles = userAdmin.getRoles("(dn=*," + rootDn + ")");
+                               long userCount = 0;
+                               long groupCount = 0;
+                               for (Role role : roles) {
+                                       if (role.getType() == Role.USER)
+                                               userCount++;
+                                       else
+                                               groupCount++;
+                               }
+                               text.append(" " + userCount + " users, " + groupCount +" groups.<br/>");
+                       } catch (InvalidSyntaxException e) {
+                               log.error("Invalid syntax", e);
+                       }
+               }
+               Label label = new Label(parent, SWT.NONE);
+               label.setData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               CmsUtils.markup(label);
+               label.setText(text.toString());
+       }
+
+       protected boolean isDeployed() {
+               return bc.getServiceReference(UserAdmin.class) != null;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/script/AppUi.java b/org.argeo.cms.ui/src/org/argeo/cms/script/AppUi.java
new file mode 100644 (file)
index 0000000..e0b2a88
--- /dev/null
@@ -0,0 +1,266 @@
+package org.argeo.cms.script;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.script.Invocable;
+import javax.script.ScriptException;
+
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.cms.util.CmsPane;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.util.SimpleErgonomics;
+import org.argeo.eclipse.ui.Selected;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.application.Application;
+import org.eclipse.rap.rwt.application.EntryPoint;
+import org.eclipse.rap.rwt.application.EntryPointFactory;
+import org.eclipse.rap.rwt.client.WebClient;
+import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.osgi.framework.BundleContext;
+
+public class AppUi implements CmsUiProvider, Branding {
+       private final CmsScriptApp app;
+
+       private CmsUiProvider ui;
+       private String createUi;
+       private Object impl;
+       private String script;
+       // private Branding branding = new Branding();
+
+       private EntryPointFactory factory;
+
+       // Branding
+       private String themeId;
+       private String additionalHeaders;
+       private String bodyHtml;
+       private String pageTitle;
+       private String pageOverflow;
+       private String favicon;
+
+       public AppUi(CmsScriptApp app) {
+               this.app = app;
+       }
+
+       public AppUi(CmsScriptApp app, String scriptPath) {
+               this.app = app;
+               this.ui = new ScriptUi((BundleContext) app.getScriptEngine().get(CmsScriptRwtApplication.BC), scriptPath);
+       }
+
+       public AppUi(CmsScriptApp app, CmsUiProvider uiProvider) {
+               this.app = app;
+               this.ui = uiProvider;
+       }
+
+       public AppUi(CmsScriptApp app, EntryPointFactory factory) {
+               this.app = app;
+               this.factory = factory;
+       }
+
+       public void apply(Repository repository, Application application, Branding appBranding, String path) {
+               Map<String, String> factoryProperties = new HashMap<>();
+               if (appBranding != null)
+                       appBranding.applyBranding(factoryProperties);
+               applyBranding(factoryProperties);
+               if (factory != null) {
+                       application.addEntryPoint("/" + path, factory, factoryProperties);
+               } else {
+                       EntryPointFactory entryPointFactory = new EntryPointFactory() {
+                               @Override
+                               public EntryPoint create() {
+                                       SimpleErgonomics ergonomics = new SimpleErgonomics(repository, "main", "/home/root/argeo:keyring",
+                                                       AppUi.this, factoryProperties);
+//                                     CmsUiProvider header = app.getHeader();
+//                                     if (header != null)
+//                                             ergonomics.setHeader(header);
+                                       app.applySides(ergonomics);
+                                       Integer headerHeight = app.getHeaderHeight();
+                                       if (headerHeight != null)
+                                               ergonomics.setHeaderHeight(headerHeight);
+                                       return ergonomics;
+                               }
+                       };
+                       application.addEntryPoint("/" + path, entryPointFactory, factoryProperties);
+               }
+       }
+
+       public void setUi(CmsUiProvider uiProvider) {
+               this.ui = uiProvider;
+       }
+
+       public void applyBranding(Map<String, String> properties) {
+               if (themeId != null)
+                       properties.put(WebClient.THEME_ID, themeId);
+               if (additionalHeaders != null)
+                       properties.put(WebClient.HEAD_HTML, additionalHeaders);
+               if (bodyHtml != null)
+                       properties.put(WebClient.BODY_HTML, bodyHtml);
+               if (pageTitle != null)
+                       properties.put(WebClient.PAGE_TITLE, pageTitle);
+               if (pageOverflow != null)
+                       properties.put(WebClient.PAGE_OVERFLOW, pageOverflow);
+               if (favicon != null)
+                       properties.put(WebClient.FAVICON, favicon);
+       }
+
+       // public Branding getBranding() {
+       // return branding;
+       // }
+
+       @Override
+       public Control createUi(Composite parent, Node context) throws RepositoryException {
+               CmsPane cmsPane = new CmsPane(parent, SWT.NONE);
+
+               if (false) {
+                       // QA
+                       CmsUtils.style(cmsPane.getQaArea(), "qa");
+                       Button reload = new Button(cmsPane.getQaArea(), SWT.FLAT);
+                       CmsUtils.style(reload, "qa");
+                       reload.setText("Reload");
+                       reload.addSelectionListener(new Selected() {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       new Thread() {
+                                               @Override
+                                               public void run() {
+                                                       app.reload();
+                                               }
+                                       }.start();
+                                       RWT.getClient().getService(JavaScriptExecutor.class)
+                                                       .execute("setTimeout('location.reload()',1000)");
+                               }
+                       });
+
+                       // Support
+                       CmsUtils.style(cmsPane.getSupportArea(), "support");
+                       Label msg = new Label(cmsPane.getSupportArea(), SWT.NONE);
+                       CmsUtils.style(msg, "support");
+                       msg.setText("UNSUPPORTED DEVELOPMENT VERSION");
+               }
+
+               if (ui != null) {
+                       ui.createUi(cmsPane.getMainArea(), context);
+               }
+               if (createUi != null) {
+                       Invocable invocable = (Invocable) app.getScriptEngine();
+                       try {
+                               invocable.invokeFunction(createUi, cmsPane.getMainArea(), context);
+
+                       } catch (NoSuchMethodException e) {
+                               // TODO Auto-generated catch block
+                               e.printStackTrace();
+                       } catch (ScriptException e) {
+                               // TODO Auto-generated catch block
+                               e.printStackTrace();
+                       }
+               }
+               if (impl != null) {
+                       Invocable invocable = (Invocable) app.getScriptEngine();
+                       try {
+                               invocable.invokeMethod(impl, "createUi", cmsPane.getMainArea(), context);
+
+                       } catch (NoSuchMethodException e) {
+                               // TODO Auto-generated catch block
+                               e.printStackTrace();
+                       } catch (ScriptException e) {
+                               // TODO Auto-generated catch block
+                               e.printStackTrace();
+                       }
+               }
+
+               // Invocable invocable = (Invocable) app.getScriptEngine();
+               // try {
+               // invocable.invokeMethod(AppUi.this, "initUi", parent, context);
+               //
+               // } catch (NoSuchMethodException e) {
+               // // TODO Auto-generated catch block
+               // e.printStackTrace();
+               // } catch (ScriptException e) {
+               // // TODO Auto-generated catch block
+               // e.printStackTrace();
+               // }
+
+               return null;
+       }
+
+       public void setCreateUi(String createUi) {
+               this.createUi = createUi;
+       }
+
+       public void setImpl(Object impl) {
+               this.impl = impl;
+       }
+
+       public Object getImpl() {
+               return impl;
+       }
+
+       public String getScript() {
+               return script;
+       }
+
+       public void setScript(String script) {
+               this.script = script;
+       }
+
+       // Branding
+       public String getThemeId() {
+               return themeId;
+       }
+
+       public void setThemeId(String themeId) {
+               this.themeId = themeId;
+       }
+
+       public String getAdditionalHeaders() {
+               return additionalHeaders;
+       }
+
+       public void setAdditionalHeaders(String additionalHeaders) {
+               this.additionalHeaders = additionalHeaders;
+       }
+
+       public String getBodyHtml() {
+               return bodyHtml;
+       }
+
+       public void setBodyHtml(String bodyHtml) {
+               this.bodyHtml = bodyHtml;
+       }
+
+       public String getPageTitle() {
+               return pageTitle;
+       }
+
+       public void setPageTitle(String pageTitle) {
+               this.pageTitle = pageTitle;
+       }
+
+       public String getPageOverflow() {
+               return pageOverflow;
+       }
+
+       public void setPageOverflow(String pageOverflow) {
+               this.pageOverflow = pageOverflow;
+       }
+
+       public String getFavicon() {
+               return favicon;
+       }
+
+       public void setFavicon(String favicon) {
+               this.favicon = favicon;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/script/Branding.java b/org.argeo.cms.ui/src/org/argeo/cms/script/Branding.java
new file mode 100644 (file)
index 0000000..2a99191
--- /dev/null
@@ -0,0 +1,22 @@
+package org.argeo.cms.script;
+
+import java.util.Map;
+
+import org.eclipse.rap.rwt.client.WebClient;
+
+public interface Branding {
+       public void applyBranding(Map<String, String> properties);
+
+       public String getThemeId();
+
+       public String getAdditionalHeaders();
+
+       public String getBodyHtml();
+
+       public String getPageTitle();
+
+       public String getPageOverflow();
+
+       public String getFavicon();
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/script/CmsScriptApp.java b/org.argeo.cms.ui/src/org/argeo/cms/script/CmsScriptApp.java
new file mode 100644 (file)
index 0000000..e639412
--- /dev/null
@@ -0,0 +1,420 @@
+package org.argeo.cms.script;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.script.ScriptEngine;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.CmsConstants;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.cms.util.BundleResourceLoader;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.util.SimpleErgonomics;
+import org.eclipse.rap.rwt.application.Application;
+import org.eclipse.rap.rwt.application.Application.OperationMode;
+import org.eclipse.rap.rwt.application.ApplicationConfiguration;
+import org.eclipse.rap.rwt.application.ExceptionHandler;
+import org.eclipse.rap.rwt.client.WebClient;
+import org.eclipse.rap.rwt.service.ResourceLoader;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+
+public class CmsScriptApp implements Branding {
+       public final static String CONTEXT_NAME = "contextName";
+
+       ServiceRegistration<ApplicationConfiguration> appConfigReg;
+
+       private ScriptEngine scriptEngine;
+
+       private final static Log log = LogFactory.getLog(CmsScriptApp.class);
+
+       private String webPath;
+       private String repo = "(cn=node)";
+
+       // private Branding branding = new Branding();
+       private Theme theme;
+
+       private List<String> resources = new ArrayList<>();
+
+       private Map<String, AppUi> ui = new HashMap<>();
+
+       private CmsUiProvider header;
+       private Integer headerHeight = null;
+       private CmsUiProvider lead;
+       private CmsUiProvider end;
+       private CmsUiProvider footer;
+
+       // Branding
+       private String themeId;
+       private String additionalHeaders;
+       private String bodyHtml;
+       private String pageTitle;
+       private String pageOverflow;
+       private String favicon;
+
+       public CmsScriptApp(ScriptEngine scriptEngine) {
+               super();
+               this.scriptEngine = scriptEngine;
+       }
+
+       public void apply(BundleContext bundleContext, Repository repository, Application application) {
+               BundleResourceLoader bundleRL = new BundleResourceLoader(bundleContext.getBundle());
+
+               application.setOperationMode(OperationMode.SWT_COMPATIBILITY);
+               // application.setOperationMode(OperationMode.JEE_COMPATIBILITY);
+
+               application.setExceptionHandler(new CmsExceptionHandler());
+
+               // loading animated gif
+               application.addResource(CmsConstants.LOADING_IMAGE, createResourceLoader(CmsConstants.LOADING_IMAGE));
+               // empty image
+               application.addResource(CmsConstants.NO_IMAGE, createResourceLoader(CmsConstants.NO_IMAGE));
+
+               for (String resource : resources) {
+                       application.addResource(resource, bundleRL);
+                       if (log.isTraceEnabled())
+                               log.trace("Resource " + resource);
+               }
+
+               if (theme != null) {
+                       theme.apply(application);
+                       String themeHeaders = theme.getAdditionalHeaders();
+                       if (themeHeaders != null) {
+                               if (additionalHeaders == null)
+                                       additionalHeaders = themeHeaders;
+                               else
+                                       additionalHeaders = themeHeaders + "\n" + additionalHeaders;
+                       }
+                       themeId = theme.getThemeId();
+               }
+
+               // client JavaScript
+               Bundle appBundle = bundleRL.getBundle();
+               BundleContext bc = appBundle.getBundleContext();
+               HttpService httpService = bc.getService(bc.getServiceReference(HttpService.class));
+               HttpContext httpContext = new BundleHttpContext(bc);
+               Enumeration<URL> themeResources = appBundle.findEntries("/js/", "*", true);
+               if (themeResources != null)
+                       bundleResources: while (themeResources.hasMoreElements()) {
+                               try {
+                                       String name = themeResources.nextElement().getPath();
+                                       if (name.endsWith("/"))
+                                               continue bundleResources;
+                                       String alias = "/" + getWebPath() + name;
+
+                                       httpService.registerResources(alias, name, httpContext);
+                                       if (log.isDebugEnabled())
+                                               log.debug("Mapped " + name + " to alias " + alias);
+
+                               } catch (NamespaceException e) {
+                                       // TODO Auto-generated catch block
+                                       e.printStackTrace();
+                               }
+                       }
+
+               // App UIs
+               for (String appUiName : ui.keySet()) {
+                       AppUi appUi = ui.get(appUiName);
+                       appUi.apply(repository, application, this, appUiName);
+
+               }
+
+       }
+
+       public void applySides(SimpleErgonomics simpleErgonomics) {
+               simpleErgonomics.setHeader(header);
+               simpleErgonomics.setLead(lead);
+               simpleErgonomics.setEnd(end);
+               simpleErgonomics.setFooter(footer);
+       }
+
+       public void register(BundleContext bundleContext, ApplicationConfiguration appConfig) {
+               Hashtable<String, String> props = new Hashtable<>();
+               props.put(CONTEXT_NAME, webPath);
+               appConfigReg = bundleContext.registerService(ApplicationConfiguration.class, appConfig, props);
+       }
+
+       public void reload() {
+               BundleContext bundleContext = appConfigReg.getReference().getBundle().getBundleContext();
+               ApplicationConfiguration appConfig = bundleContext.getService(appConfigReg.getReference());
+               appConfigReg.unregister();
+               register(bundleContext, appConfig);
+
+               // BundleContext bundleContext = (BundleContext)
+               // getScriptEngine().get("bundleContext");
+               // try {
+               // Bundle bundle = bundleContext.getBundle();
+               // bundle.stop();
+               // bundle.start();
+               // } catch (BundleException e) {
+               // // TODO Auto-generated catch block
+               // e.printStackTrace();
+               // }
+       }
+
+       private static ResourceLoader createResourceLoader(final String resourceName) {
+               return new ResourceLoader() {
+                       public InputStream getResourceAsStream(String resourceName) throws IOException {
+                               return getClass().getClassLoader().getResourceAsStream(resourceName);
+                       }
+               };
+       }
+
+       public List<String> getResources() {
+               return resources;
+       }
+
+       public AppUi newUi(String name) {
+               if (ui.containsKey(name))
+                       throw new IllegalArgumentException("There is already an UI named " + name);
+               AppUi appUi = new AppUi(this);
+               // appUi.setApp(this);
+               ui.put(name, appUi);
+               return appUi;
+       }
+
+       public void addUi(String name, AppUi appUi) {
+               if (ui.containsKey(name))
+                       throw new IllegalArgumentException("There is already an UI named " + name);
+               // appUi.setApp(this);
+               ui.put(name, appUi);
+       }
+
+       public void applyBranding(Map<String, String> properties) {
+               if (themeId != null)
+                       properties.put(WebClient.THEME_ID, themeId);
+               if (additionalHeaders != null)
+                       properties.put(WebClient.HEAD_HTML, additionalHeaders);
+               if (bodyHtml != null)
+                       properties.put(WebClient.BODY_HTML, bodyHtml);
+               if (pageTitle != null)
+                       properties.put(WebClient.PAGE_TITLE, pageTitle);
+               if (pageOverflow != null)
+                       properties.put(WebClient.PAGE_OVERFLOW, pageOverflow);
+               if (favicon != null)
+                       properties.put(WebClient.FAVICON, favicon);
+       }
+
+       class CmsExceptionHandler implements ExceptionHandler {
+
+               @Override
+               public void handleException(Throwable throwable) {
+                       // TODO be smarter
+                       CmsUtils.getCmsView().exception(throwable);
+               }
+
+       }
+
+       // public Branding getBranding() {
+       // return branding;
+       // }
+
+       ScriptEngine getScriptEngine() {
+               return scriptEngine;
+       }
+
+       public static String toJson(Node node) {
+               try {
+                       StringBuilder sb = new StringBuilder();
+                       sb.append('{');
+                       PropertyIterator pit = node.getProperties();
+                       int count = 0;
+                       while (pit.hasNext()) {
+                               Property p = pit.nextProperty();
+                               int type = p.getType();
+                               if (type == PropertyType.REFERENCE || type == PropertyType.WEAKREFERENCE || type == PropertyType.PATH) {
+                                       Node ref = p.getNode();
+                                       if (count != 0)
+                                               sb.append(',');
+                                       // TODO limit depth?
+                                       sb.append(toJson(ref));
+                                       count++;
+                               } else if (!p.isMultiple()) {
+                                       if (count != 0)
+                                               sb.append(',');
+                                       sb.append('\"').append(p.getName()).append("\":\"").append(p.getString()).append('\"');
+                                       count++;
+                               }
+                       }
+                       sb.append('}');
+                       return sb.toString();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot convert " + node + " to JSON", e);
+               }
+       }
+
+       public void fromJson(Node node, String json) {
+               // TODO
+       }
+
+       public Theme getTheme() {
+               return theme;
+       }
+
+       public void setTheme(Theme theme) {
+               this.theme = theme;
+       }
+
+       public String getWebPath() {
+               return webPath;
+       }
+
+       public void setWebPath(String context) {
+               this.webPath = context;
+       }
+
+       public String getRepo() {
+               return repo;
+       }
+
+       public void setRepo(String repo) {
+               this.repo = repo;
+       }
+
+       public Map<String, AppUi> getUi() {
+               return ui;
+       }
+
+       public void setUi(Map<String, AppUi> ui) {
+               this.ui = ui;
+       }
+
+       // Branding
+       public String getThemeId() {
+               return themeId;
+       }
+
+       public void setThemeId(String themeId) {
+               this.themeId = themeId;
+       }
+
+       public String getAdditionalHeaders() {
+               return additionalHeaders;
+       }
+
+       public void setAdditionalHeaders(String additionalHeaders) {
+               this.additionalHeaders = additionalHeaders;
+       }
+
+       public String getBodyHtml() {
+               return bodyHtml;
+       }
+
+       public void setBodyHtml(String bodyHtml) {
+               this.bodyHtml = bodyHtml;
+       }
+
+       public String getPageTitle() {
+               return pageTitle;
+       }
+
+       public void setPageTitle(String pageTitle) {
+               this.pageTitle = pageTitle;
+       }
+
+       public String getPageOverflow() {
+               return pageOverflow;
+       }
+
+       public void setPageOverflow(String pageOverflow) {
+               this.pageOverflow = pageOverflow;
+       }
+
+       public String getFavicon() {
+               return favicon;
+       }
+
+       public void setFavicon(String favicon) {
+               this.favicon = favicon;
+       }
+
+       public CmsUiProvider getHeader() {
+               return header;
+       }
+
+       public void setHeader(CmsUiProvider header) {
+               this.header = header;
+       }
+
+       public Integer getHeaderHeight() {
+               return headerHeight;
+       }
+
+       public void setHeaderHeight(Integer headerHeight) {
+               this.headerHeight = headerHeight;
+       }
+
+       public CmsUiProvider getLead() {
+               return lead;
+       }
+
+       public void setLead(CmsUiProvider lead) {
+               this.lead = lead;
+       }
+
+       public CmsUiProvider getEnd() {
+               return end;
+       }
+
+       public void setEnd(CmsUiProvider end) {
+               this.end = end;
+       }
+
+       public CmsUiProvider getFooter() {
+               return footer;
+       }
+
+       public void setFooter(CmsUiProvider footer) {
+               this.footer = footer;
+       }
+
+       static class BundleHttpContext implements HttpContext {
+               private BundleContext bundleContext;
+
+               public BundleHttpContext(BundleContext bundleContext) {
+                       super();
+                       this.bundleContext = bundleContext;
+               }
+
+               @Override
+               public boolean handleSecurity(HttpServletRequest request, HttpServletResponse response) throws IOException {
+                       // TODO Auto-generated method stub
+                       return true;
+               }
+
+               @Override
+               public URL getResource(String name) {
+
+                       return bundleContext.getBundle().getEntry(name);
+               }
+
+               @Override
+               public String getMimeType(String name) {
+                       return null;
+               }
+
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/script/CmsScriptRwtApplication.java b/org.argeo.cms.ui/src/org/argeo/cms/script/CmsScriptRwtApplication.java
new file mode 100644 (file)
index 0000000..1bc2b0b
--- /dev/null
@@ -0,0 +1,104 @@
+package org.argeo.cms.script;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URL;
+
+import javax.jcr.Repository;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.eclipse.rap.rwt.application.Application;
+import org.eclipse.rap.rwt.application.ApplicationConfiguration;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.wiring.BundleWiring;
+
+public class CmsScriptRwtApplication implements ApplicationConfiguration {
+       public final static String APP = "APP";
+       public final static String BC = "BC";
+
+       private final Log log = LogFactory.getLog(CmsScriptRwtApplication.class);
+
+       BundleContext bundleContext;
+       Repository repository;
+
+       ScriptEngine engine;
+
+       public void init(BundleContext bundleContext) {
+               this.bundleContext = bundleContext;
+               // System.out.println("bundleContext=" + bundleContext);
+               // System.out.println("repository=" + repository);
+               ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
+               ClassLoader bundleCl = bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader();
+               ClassLoader currentCcl = Thread.currentThread().getContextClassLoader();
+               try {
+                       Thread.currentThread().setContextClassLoader(bundleCl);
+                       engine = scriptEngineManager.getEngineByName("JavaScript");
+               } catch (Exception e) {
+                       e.printStackTrace();
+               } finally {
+                       Thread.currentThread().setContextClassLoader(currentCcl);
+               }
+
+               // Load script
+               URL appUrl = bundleContext.getBundle().getEntry("cms/app.js");
+               // System.out.println("Loading " + appUrl);
+               // System.out.println("Loading " + appUrl.getHost());
+               // System.out.println("Loading " + appUrl.getPath());
+
+               CmsScriptApp app = new CmsScriptApp(engine);
+               engine.put(APP, app);
+               engine.put(BC, bundleContext);
+               try (Reader reader = new InputStreamReader(appUrl.openStream())) {
+                       engine.eval(reader);
+               } catch (IOException | ScriptException e) {
+                       throw new CmsException("Cannot execute " + appUrl, e);
+               }
+
+               if (log.isDebugEnabled())
+                       log.debug("CMS script app initialized from " + appUrl);
+
+       }
+
+       public void destroy(BundleContext bundleContext) {
+               engine = null;
+       }
+
+       @Override
+       public void configure(Application application) {
+               load(application);
+       }
+
+       void load(Application application) {
+               CmsScriptApp app = getApp();
+               app.apply(bundleContext, repository, application);
+               if (log.isDebugEnabled())
+                       log.debug("CMS script app loaded to " + app.getWebPath());
+       }
+
+       CmsScriptApp getApp() {
+               if (engine == null)
+                       throw new IllegalStateException("CMS script app is not initialized");
+               return (CmsScriptApp) engine.get(APP);
+       }
+
+       void update() {
+
+               try {
+                       bundleContext.getBundle().update();
+               } catch (BundleException e) {
+                       e.printStackTrace();
+               }
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/script/ScriptAppActivator.java b/org.argeo.cms.ui/src/org/argeo/cms/script/ScriptAppActivator.java
new file mode 100644 (file)
index 0000000..7b3c57e
--- /dev/null
@@ -0,0 +1,37 @@
+package org.argeo.cms.script;
+
+import javax.jcr.Repository;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class ScriptAppActivator implements BundleActivator {
+       @Override
+       public void start(BundleContext context) throws Exception {
+               CmsScriptRwtApplication appConfig = new CmsScriptRwtApplication();
+               appConfig.init(context);
+               CmsScriptApp app = appConfig.getApp();
+               ServiceTracker<Repository, Repository> repoSt = new ServiceTracker<Repository, Repository>(context,
+                               FrameworkUtil.createFilter("(&" + app.getRepo() + "(objectClass=javax.jcr.Repository))"), null) {
+
+                       @Override
+                       public Repository addingService(ServiceReference<Repository> reference) {
+                               Repository repository = super.addingService(reference);
+                               appConfig.setRepository(repository);
+                               CmsScriptApp app = appConfig.getApp();
+                               app.register(context, appConfig);
+                               return repository;
+                       }
+
+               };
+               repoSt.open();
+       }
+
+       @Override
+       public void stop(BundleContext context) throws Exception {
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/script/ScriptUi.java b/org.argeo.cms.ui/src/org/argeo/cms/script/ScriptUi.java
new file mode 100644 (file)
index 0000000..a15c6c9
--- /dev/null
@@ -0,0 +1,115 @@
+package org.argeo.cms.script;
+
+import java.net.URL;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.wiring.BundleWiring;
+
+public class ScriptUi implements CmsUiProvider {
+       private final static Log log = LogFactory.getLog(ScriptUi.class);
+
+       private boolean development = true;
+       private ScriptEngine scriptEngine;
+
+       private URL appUrl;
+       // private BundleContext bundleContext;
+       // private String path;
+
+       // private Bindings bindings;
+       // private String script;
+
+       public ScriptUi(BundleContext bundleContext, String path) {
+               ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
+               ClassLoader bundleCl = bundleContext.getBundle().adapt(BundleWiring.class).getClassLoader();
+               ClassLoader currentCcl = Thread.currentThread().getContextClassLoader();
+               try {
+                       Thread.currentThread().setContextClassLoader(bundleCl);
+                       scriptEngine = scriptEngineManager.getEngineByName("JavaScript");
+                       scriptEngine.put(CmsScriptRwtApplication.BC, bundleContext);
+               } catch (Exception e) {
+                       e.printStackTrace();
+               } finally {
+                       Thread.currentThread().setContextClassLoader(currentCcl);
+               }
+               this.appUrl = bundleContext.getBundle().getEntry(path);
+               load();
+       }
+
+       private void load() {
+//             try (Reader reader = new InputStreamReader(appUrl.openStream())) {
+//                     scriptEngine.eval(reader);
+//             } catch (IOException | ScriptException e) {
+//                     log.warn("Cannot execute " + appUrl, e);
+//             }
+
+               try {
+                       scriptEngine.eval("load('" + appUrl + "')");
+               } catch (ScriptException e) {
+                       log.warn("Cannot execute " + appUrl, e);
+               }
+
+       }
+
+       // public ScriptUiProvider(ScriptEngine scriptEngine, String script) throws
+       // ScriptException {
+       // super();
+       // this.scriptEngine = scriptEngine;
+       // this.script = script;
+       // bindings = scriptEngine.createBindings();
+       // scriptEngine.eval(script, bindings);
+       // }
+
+       @Override
+       public Control createUi(Composite parent, Node context) throws RepositoryException {
+               long begin = System.currentTimeMillis();
+               // if (bindings == null) {
+               // bindings = scriptEngine.createBindings();
+               // try {
+               // scriptEngine.eval(script, bindings);
+               // } catch (ScriptException e) {
+               // log.warn("Cannot evaluate script", e);
+               // }
+               // }
+               // Bindings bindings = scriptEngine.createBindings();
+               // bindings.put("parent", parent);
+               // bindings.put("context", context);
+               // URL appUrl = bundleContext.getBundle().getEntry(path);
+               // try (Reader reader = new InputStreamReader(appUrl.openStream())) {
+               // scriptEngine.eval(reader,bindings);
+               // } catch (IOException | ScriptException e) {
+               // log.warn("Cannot execute " + appUrl, e);
+               // }
+
+               if (development)
+                       load();
+
+               Invocable invocable = (Invocable) scriptEngine;
+               try {
+                       invocable.invokeFunction("createUi", parent, context);
+               } catch (NoSuchMethodException e) {
+                       // TODO Auto-generated catch block
+                       e.printStackTrace();
+               } catch (ScriptException e) {
+                       // TODO Auto-generated catch block
+                       e.printStackTrace();
+               }
+
+               long duration = System.currentTimeMillis() - begin;
+               if (log.isTraceEnabled())
+                       log.trace(appUrl + " UI in " + duration + " ms");
+               return null;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/script/Theme.java b/org.argeo.cms.ui/src/org/argeo/cms/script/Theme.java
new file mode 100644 (file)
index 0000000..3fa4bc4
--- /dev/null
@@ -0,0 +1,184 @@
+package org.argeo.cms.script;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.util.BundleResourceLoader;
+import org.argeo.cms.util.ThemeUtils;
+import org.eclipse.rap.rwt.application.Application;
+import org.eclipse.rap.rwt.service.ResourceLoader;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+public class Theme {
+       private final static Log log = LogFactory.getLog(Theme.class);
+
+       private final String themeId;
+       private Map<String, ResourceLoader> css = new HashMap<>();
+       private Map<String, ResourceLoader> resources = new HashMap<>();
+
+       private String headerCss;
+       private List<String> fonts = new ArrayList<>();
+
+       private String basePath;
+       private String cssPath;
+
+       public Theme(BundleContext bundleContext) {
+               this(bundleContext, null);
+       }
+
+       public Theme(BundleContext bundleContext, String symbolicName) {
+               Bundle themeBundle;
+               if (symbolicName == null) {
+                       themeBundle = bundleContext.getBundle();
+                       basePath = "/theme/internal/";
+                       cssPath = basePath;
+               } else {
+                       themeBundle = ThemeUtils.findThemeBundle(bundleContext, symbolicName);
+                       basePath = "/";
+                       cssPath = "/rap/";
+               }
+               this.themeId = themeBundle.getSymbolicName();
+               addStyleSheets(themeBundle, new BundleResourceLoader(themeBundle));
+               BundleResourceLoader themeBRL = new BundleResourceLoader(themeBundle);
+               addResources(themeBRL, "*.png");
+               addResources(themeBRL, "*.gif");
+               addResources(themeBRL, "*.jpg");
+               addResources(themeBRL, "*.jpeg");
+               addResources(themeBRL, "*.svg");
+               addResources(themeBRL, "*.ico");
+
+               // fonts
+               URL fontsUrl = themeBundle.getEntry(basePath + "fonts.txt");
+               if (fontsUrl != null) {
+                       loadFontsUrl(fontsUrl);
+               }
+
+               // common CSS header (plain CSS)
+               URL headerCssUrl = themeBundle.getEntry(basePath + "header.css");
+               if (headerCssUrl != null) {
+                       try (BufferedReader buffer = new BufferedReader(new InputStreamReader(headerCssUrl.openStream(), UTF_8))) {
+                               headerCss = buffer.lines().collect(Collectors.joining("\n"));
+                       } catch (IOException e) {
+                               throw new CmsException("Cannot read " + headerCssUrl, e);
+                       }
+               }
+
+       }
+
+       public void apply(Application application) {
+               for (String name : resources.keySet()) {
+                       application.addResource(name, resources.get(name));
+                       if (log.isDebugEnabled())
+                               log.debug("Added resource " + name);
+               }
+               for (String name : css.keySet()) {
+                       application.addStyleSheet(themeId, name, css.get(name));
+                       if (log.isDebugEnabled())
+                               log.debug("Added RAP CSS " + name);
+               }
+       }
+
+       public String getAdditionalHeaders() {
+               StringBuilder sb = new StringBuilder();
+               if (headerCss != null) {
+                       sb.append("<style type='text/css'>\n");
+                       sb.append(headerCss);
+                       sb.append("\n</style>\n");
+               }
+               for (String link : fonts) {
+                       sb.append("<link rel='stylesheet' href='");
+                       sb.append(link);
+                       sb.append("'/>\n");
+               }
+               if (sb.length() == 0)
+                       return null;
+               else
+                       return sb.toString();
+       }
+
+       void addStyleSheets(Bundle themeBundle, ResourceLoader ssRL) {
+               Enumeration<URL> themeResources = themeBundle.findEntries(cssPath, "*.css", true);
+               if (themeResources == null)
+                       return;
+               while (themeResources.hasMoreElements()) {
+                       String resource = themeResources.nextElement().getPath();
+                       // remove first '/' so that RWT registers it
+                       resource = resource.substring(1);
+                       if (!resource.endsWith("/")) {
+                               if (css.containsKey(resource))
+                                       log.warn("Overriding " + resource + " from " + themeBundle.getSymbolicName());
+                               css.put(resource, ssRL);
+                       }
+
+               }
+
+       }
+
+       void loadFontsUrl(URL url) {
+               try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream(), UTF_8))) {
+                       String line = null;
+                       while ((line = in.readLine()) != null) {
+                               line = line.trim();
+                               if (!line.equals("") && !line.startsWith("#")) {
+                                       fonts.add(line);
+                               }
+                       }
+               } catch (IOException e) {
+                       throw new CmsException("Cannot load URL " + url, e);
+               }
+       }
+
+       void addResources(BundleResourceLoader themeBRL, String pattern) {
+               Bundle themeBundle = themeBRL.getBundle();
+               Enumeration<URL> themeResources = themeBundle.findEntries(basePath, pattern, true);
+               if (themeResources == null)
+                       return;
+               while (themeResources.hasMoreElements()) {
+                       String resource = themeResources.nextElement().getPath();
+                       // remove first '/' so that RWT registers it
+                       resource = resource.substring(1);
+                       if (!resource.endsWith("/")) {
+                               if (resources.containsKey(resource))
+                                       log.warn("Overriding " + resource + " from " + themeBundle.getSymbolicName());
+                               resources.put(resource, themeBRL);
+                       }
+
+               }
+
+       }
+
+       public String getThemeId() {
+               return themeId;
+       }
+
+       public String getBasePath() {
+               return basePath;
+       }
+
+       public void setBasePath(String basePath) {
+               this.basePath = basePath;
+       }
+
+       public String getCssPath() {
+               return cssPath;
+       }
+
+       public void setCssPath(String cssPath) {
+               this.cssPath = cssPath;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/script/cms.js b/org.argeo.cms.ui/src/org/argeo/cms/script/cms.js
new file mode 100644 (file)
index 0000000..ac2eecf
--- /dev/null
@@ -0,0 +1,90 @@
+// CMS
+var ScrolledPage = Java.type('org.argeo.cms.widgets.ScrolledPage');
+
+var CmsScriptApp = Java.type('org.argeo.cms.script.CmsScriptApp');
+var AppUi = Java.type('org.argeo.cms.script.AppUi');
+var Theme = Java.type('org.argeo.cms.script.Theme');
+var ScriptUi = Java.type('org.argeo.cms.script.ScriptUi');
+var CmsUtils = Java.type('org.argeo.cms.util.CmsUtils');
+var SimpleCmsHeader = Java.type('org.argeo.cms.util.SimpleCmsHeader');
+var CmsLink = Java.type('org.argeo.cms.util.CmsLink');
+var MenuLink = Java.type('org.argeo.cms.util.MenuLink');
+var UserMenuLink = Java.type('org.argeo.cms.util.UserMenuLink');
+
+// SWT
+var SWT = Java.type('org.eclipse.swt.SWT');
+var Composite = Java.type('org.eclipse.swt.widgets.Composite');
+var Label = Java.type('org.eclipse.swt.widgets.Label');
+var Button = Java.type('org.eclipse.swt.widgets.Button');
+var Text = Java.type('org.eclipse.swt.widgets.Text');
+var Browser = Java.type('org.eclipse.swt.browser.Browser');
+
+var FillLayout = Java.type('org.eclipse.swt.layout.FillLayout');
+var GridLayout = Java.type('org.eclipse.swt.layout.GridLayout');
+var RowLayout = Java.type('org.eclipse.swt.layout.RowLayout');
+var FormLayout = Java.type('org.eclipse.swt.layout.FormLayout');
+var GridData = Java.type('org.eclipse.swt.layout.GridData');
+
+function loadNode(node) {
+       var json = CmsScriptApp.toJson(node)
+       var fromJson = JSON.parse(json)
+       return fromJson
+}
+
+function newArea(parent, style, layout) {
+       var control = new Composite(parent, SWT.NONE)
+       control.setLayout(layout)
+       CmsUtils.style(control, style)
+       return control
+}
+
+function newLabel(parent, style, text) {
+       var control = new Label(parent, SWT.WRAP)
+       control.setText(text)
+       CmsUtils.style(control, style)
+       CmsUtils.markup(control)
+       return control
+}
+
+function newButton(parent, style, text) {
+       var control = new Button(parent, SWT.FLAT)
+       control.setText(text)
+       CmsUtils.style(control, style)
+       CmsUtils.markup(control)
+       return control
+}
+
+function newFormLabel(parent, style, text) {
+       return newLabel(parent, style, '<b>' + text + '</b>')
+}
+
+function newText(parent, style, msg) {
+       var control = new Text(parent, SWT.NONE)
+       control.setMessage(msg)
+       CmsUtils.style(control, style)
+       return control
+}
+
+function newScrolledPage(parent) {
+       var scrolled = new ScrolledPage(parent, SWT.NONE)
+       scrolled.setLayoutData(CmsUtils.fillAll())
+       scrolled.setLayout(CmsUtils.noSpaceGridLayout())
+       var page = new Composite(scrolled, SWT.NONE)
+       page.setLayout(CmsUtils.noSpaceGridLayout())
+       page.setBackgroundMode(SWT.INHERIT_NONE)
+       return page
+}
+
+function gridData(control) {
+       var gridData = new GridData()
+       control.setLayoutData(gridData)
+       return gridData
+}
+
+function gridData(control, hAlign, vAlign) {
+       var gridData = new GridData(hAlign, vAlign, false, false)
+       control.setLayoutData(gridData)
+       return gridData
+}
+
+// print(__FILE__, __LINE__, __DIR__)
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/CustomTextEditor.java b/org.argeo.cms.ui/src/org/argeo/cms/text/CustomTextEditor.java
new file mode 100644 (file)
index 0000000..442ad78
--- /dev/null
@@ -0,0 +1,35 @@
+package org.argeo.cms.text;
+
+import static org.argeo.cms.util.CmsUtils.fillWidth;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.ui.internal.text.AbstractTextViewer;
+import org.argeo.cms.viewers.Section;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Manages hardcoded sections as an arbitrary hierarchy under the main section,
+ * which contains no text and no title.
+ */
+public class CustomTextEditor extends AbstractTextViewer {
+       private static final long serialVersionUID = 5277789504209413500L;
+
+       public CustomTextEditor(Composite parent, int style, Node textNode,
+                       CmsEditable cmsEditable) throws RepositoryException {
+               this(new Section(parent, style, textNode), style, cmsEditable);
+       }
+
+       public CustomTextEditor(Section mainSection, int style,
+                       CmsEditable cmsEditable) throws RepositoryException {
+               super(mainSection, style, cmsEditable);
+               mainSection.setLayoutData(fillWidth());
+       }
+
+       @Override
+       public Section getMainSection() {
+               return super.getMainSection();
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/DbkTextInterpreter.java b/org.argeo.cms.ui/src/org/argeo/cms/text/DbkTextInterpreter.java
new file mode 100644 (file)
index 0000000..aa32b3b
--- /dev/null
@@ -0,0 +1,96 @@
+package org.argeo.cms.text;
+
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.CmsException;
+import org.argeo.jcr.docbook.DocBookNames;
+import org.argeo.jcr.docbook.DocBookTypes;
+
+/** Based on HTML with a few Wiki-like shortcuts. */
+public class DbkTextInterpreter implements TextInterpreter {
+
+       @Override
+       public void write(Item item, String content) {
+               try {
+                       if (item instanceof Node) {
+                               Node node = (Node) item;
+                               if (node.isNodeType(DocBookTypes.PARA)) {
+                                       String raw = convertToStorage(node, content);
+                                       validateBeforeStoring(raw);
+                                       Node jcrText;
+                                       if (!node.hasNode(DocBookNames.JCR_XMLTEXT))
+                                               jcrText = node.addNode(DocBookNames.JCR_XMLTEXT, DocBookTypes.XMLTEXT);
+                                       else
+                                               jcrText = node.getNode(DocBookNames.JCR_XMLTEXT);
+                                       jcrText.setProperty(DocBookNames.JCR_XMLCHARACTERS, raw);
+                               } else {
+                                       throw new CmsException("Don't know how to interpret " + node);
+                               }
+                       } else {// property
+                               Property property = (Property) item;
+                               property.setValue(content);
+                       }
+                       // item.getSession().save();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot set content on " + item, e);
+               }
+       }
+
+       @Override
+       public String read(Item item) {
+               try {
+                       String raw = raw(item);
+                       return convertFromStorage(item, raw);
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot get " + item + " for edit", e);
+               }
+       }
+
+       @Override
+       public String raw(Item item) {
+               try {
+                       item.getSession().refresh(true);
+                       if (item instanceof Node) {
+                               Node node = (Node) item;
+                               if (node.isNodeType(DocBookTypes.PARA)) {
+                                       // WORKAROUND FOR BROKEN PARARAPHS
+                                       // if (!node.hasProperty(CMS_CONTENT)) {
+                                       // node.setProperty(CMS_CONTENT, "");
+                                       // node.getSession().save();
+                                       // }
+                                       Node jcrText = node.getNode(DocBookNames.JCR_XMLTEXT);
+                                       return jcrText.getProperty(DocBookNames.JCR_XMLCHARACTERS).getString();
+                               } else {
+                                       throw new CmsException("Don't know how to interpret " + node);
+                               }
+                       } else {// property
+                               Property property = (Property) item;
+                               return property.getString();
+                       }
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot get " + item + " content", e);
+               }
+       }
+
+       // EXTENSIBILITY
+       /**
+        * To be overridden, in order to make sure that only valid strings are being
+        * stored.
+        */
+       protected void validateBeforeStoring(String raw) {
+       }
+
+       /** To be overridden, in order to support additional formatting. */
+       protected String convertToStorage(Item item, String content) throws RepositoryException {
+               return content;
+
+       }
+
+       /** To be overridden, in order to support additional formatting. */
+       protected String convertFromStorage(Item item, String content) throws RepositoryException {
+               return content;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/DocumentPage.java b/org.argeo.cms.ui/src/org/argeo/cms/text/DocumentPage.java
new file mode 100644 (file)
index 0000000..824add3
--- /dev/null
@@ -0,0 +1,61 @@
+package org.argeo.cms.text;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.cms.util.CmsLink;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.JcrVersionCmsEditable;
+import org.argeo.cms.widgets.ScrolledPage;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.docbook.DocBookTypes;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * Display the text of the context, and provide an editor if the user can edit.
+ */
+public class DocumentPage implements CmsUiProvider {
+       @Override
+       public Control createUi(Composite parent, Node context) throws RepositoryException {
+               CmsEditable cmsEditable = new JcrVersionCmsEditable(context);
+               if (cmsEditable.canEdit())
+                       new TextEditorHeader(cmsEditable, parent, SWT.NONE).setLayoutData(CmsUtils.fillWidth());
+
+               ScrolledPage page = new ScrolledPage(parent, SWT.NONE);
+               page.setLayout(CmsUtils.noSpaceGridLayout());
+               GridData textGd = CmsUtils.fillAll();
+               page.setLayoutData(textGd);
+
+               if (context.isNodeType(DocBookTypes.ARTICLE)) {
+                       new DocumentTextEditor(page, SWT.NONE, context, cmsEditable);
+               } else {
+                       parent.setBackgroundMode(SWT.INHERIT_NONE);
+                       if (context.getSession().hasPermission(context.getPath(), Session.ACTION_ADD_NODE)) {
+                               Node indexNode = JcrUtils.getOrAdd(context, CmsNames.CMS_INDEX, DocBookTypes.ARTICLE);
+                               new DocumentTextEditor(page, SWT.NONE, indexNode, cmsEditable);
+                               textGd.heightHint = 400;
+
+                               for (NodeIterator ni = context.getNodes(); ni.hasNext();) {
+                                       Node textNode = ni.nextNode();
+                                       if (textNode.isNodeType(NodeType.NT_FOLDER))
+                                               new CmsLink(textNode.getName() + "/", textNode.getPath()).createUi(parent, textNode);
+                               }
+                               for (NodeIterator ni = context.getNodes(); ni.hasNext();) {
+                                       Node textNode = ni.nextNode();
+                                       if (textNode.isNodeType(DocBookTypes.ARTICLE) && !textNode.getName().equals(CmsNames.CMS_INDEX))
+                                               new CmsLink(textNode.getName(), textNode.getPath()).createUi(parent, textNode);
+                               }
+                       }
+               }
+               return page;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/DocumentTextEditor.java b/org.argeo.cms.ui/src/org/argeo/cms/text/DocumentTextEditor.java
new file mode 100644 (file)
index 0000000..1b1e5c4
--- /dev/null
@@ -0,0 +1,41 @@
+package org.argeo.cms.text;
+
+import static javax.jcr.Property.JCR_TITLE;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.ui.internal.text.AbstractDbkViewer;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.jcr.docbook.DocBookNames;
+import org.argeo.jcr.docbook.DocBookTypes;
+import org.eclipse.swt.widgets.Composite;
+
+/** Text editor where sections and subsections can be managed by the user. */
+public class DocumentTextEditor extends AbstractDbkViewer {
+       private static final long serialVersionUID = 6049661610883342325L;
+
+       public DocumentTextEditor(Composite parent, int style, Node textNode, CmsEditable cmsEditable)
+                       throws RepositoryException {
+               super(new TextSection(parent, style, textNode), style, cmsEditable);
+               refresh();
+               getMainSection().setLayoutData(CmsUtils.fillWidth());
+       }
+
+       @Override
+       protected void initModel(Node textNode) throws RepositoryException {
+               if (isFlat())
+                       textNode.addNode(DocBookNames.DBK_PARA, DocBookTypes.PARA);
+               else
+                       textNode.setProperty(JCR_TITLE, textNode.getName());
+       }
+
+       @Override
+       protected Boolean isModelInitialized(Node textNode) throws RepositoryException {
+               return textNode.hasProperty(Property.JCR_TITLE) || textNode.hasNode(DocBookNames.DBK_PARA)
+                               || (!isFlat() && textNode.hasNode(DocBookNames.DBK_SECTION));
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/IdentityTextInterpreter.java b/org.argeo.cms.ui/src/org/argeo/cms/text/IdentityTextInterpreter.java
new file mode 100644 (file)
index 0000000..79f6ede
--- /dev/null
@@ -0,0 +1,95 @@
+package org.argeo.cms.text;
+
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.CmsTypes;
+
+/** Based on HTML with a few Wiki-like shortcuts. */
+public class IdentityTextInterpreter implements TextInterpreter, CmsNames {
+
+       @Override
+       public void write(Item item, String content) {
+               try {
+                       if (item instanceof Node) {
+                               Node node = (Node) item;
+                               if (node.isNodeType(CmsTypes.CMS_STYLED)) {
+                                       String raw = convertToStorage(node, content);
+                                       validateBeforeStoring(raw);
+                                       node.setProperty(CMS_CONTENT, raw);
+                               } else {
+                                       throw new CmsException("Don't know how to interpret "
+                                                       + node);
+                               }
+                       } else {// property
+                               Property property = (Property) item;
+                               property.setValue(content);
+                       }
+                       // item.getSession().save();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot set content on " + item, e);
+               }
+       }
+
+       @Override
+       public String read(Item item) {
+               try {
+                       String raw = raw(item);
+                       return convertFromStorage(item, raw);
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot get " + item + " for edit", e);
+               }
+       }
+
+       @Override
+       public String raw(Item item) {
+               try {
+                       item.getSession().refresh(true);
+                       if (item instanceof Node) {
+                               Node node = (Node) item;
+                               if (node.isNodeType(CmsTypes.CMS_STYLED)) {
+                                       // WORKAROUND FOR BROKEN PARARAPHS
+                                       if (!node.hasProperty(CMS_CONTENT)) {
+                                               node.setProperty(CMS_CONTENT, "");
+                                               node.getSession().save();
+                                       }
+
+                                       return node.getProperty(CMS_CONTENT).getString();
+                               } else {
+                                       throw new CmsException("Don't know how to interpret "
+                                                       + node);
+                               }
+                       } else {// property
+                               Property property = (Property) item;
+                               return property.getString();
+                       }
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot get " + item + " content", e);
+               }
+       }
+
+       // EXTENSIBILITY
+       /**
+        * To be overridden, in order to make sure that only valid strings are being
+        * stored.
+        */
+       protected void validateBeforeStoring(String raw) {
+       }
+
+       /** To be overridden, in order to support additional formatting. */
+       protected String convertToStorage(Item item, String content)
+                       throws RepositoryException {
+               return content;
+
+       }
+
+       /** To be overridden, in order to support additional formatting. */
+       protected String convertFromStorage(Item item, String content)
+                       throws RepositoryException {
+               return content;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/Img.java b/org.argeo.cms.ui/src/org/argeo/cms/text/Img.java
new file mode 100644 (file)
index 0000000..12f65f3
--- /dev/null
@@ -0,0 +1,151 @@
+package org.argeo.cms.text;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.CmsImageManager;
+import org.argeo.cms.ui.internal.JcrFileUploadReceiver;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.NodePart;
+import org.argeo.cms.viewers.Section;
+import org.argeo.cms.viewers.SectionPart;
+import org.argeo.cms.widgets.EditableImage;
+import org.eclipse.rap.fileupload.FileUploadHandler;
+import org.eclipse.rap.fileupload.FileUploadListener;
+import org.eclipse.rap.fileupload.FileUploadReceiver;
+import org.eclipse.rap.rwt.service.ServerPushSession;
+import org.eclipse.rap.rwt.widgets.FileUpload;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/** An image within the Argeo Text framework */
+public class Img extends EditableImage implements SectionPart, NodePart {
+       private static final long serialVersionUID = 6233572783968188476L;
+
+       private final Section section;
+
+       private final CmsImageManager imageManager;
+       private FileUploadHandler currentUploadHandler = null;
+       private FileUploadListener fileUploadListener;
+
+       public Img(Composite parent, int swtStyle, Node imgNode,
+                       Point preferredImageSize) throws RepositoryException {
+               this(Section.findSection(parent), parent, swtStyle, imgNode,
+                               preferredImageSize);
+               setStyle(TextStyles.TEXT_IMAGE);
+       }
+
+       public Img(Composite parent, int swtStyle, Node imgNode)
+                       throws RepositoryException {
+               this(Section.findSection(parent), parent, swtStyle, imgNode, null);
+               setStyle(TextStyles.TEXT_IMAGE);
+       }
+
+       Img(Section section, Composite parent, int swtStyle, Node imgNode,
+                       Point preferredImageSize) throws RepositoryException {
+               super(parent, swtStyle, imgNode, false, preferredImageSize);
+               this.section = section;
+               imageManager = CmsUtils.getCmsView().getImageManager();
+               CmsUtils.style(this, TextStyles.TEXT_IMG);
+       }
+
+       @Override
+       protected Control createControl(Composite box, String style) {
+               if (isEditing()) {
+                       try {
+                               return createImageChooser(box, style);
+                       } catch (RepositoryException e) {
+                               throw new CmsException("Cannot create image chooser", e);
+                       }
+               } else {
+                       return createLabel(box, style);
+               }
+       }
+
+       @Override
+       public synchronized void stopEditing() {
+               super.stopEditing();
+               fileUploadListener = null;
+       }
+
+       @Override
+       protected synchronized Boolean load(Control lbl) {
+               try {
+                       Node imgNode = getNode();
+                       boolean loaded = imageManager.load(imgNode, lbl,
+                                       getPreferredImageSize());
+                       // getParent().layout();
+                       return loaded;
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot load " + getNodeId()
+                                       + " from image manager", e);
+               }
+       }
+
+       protected Control createImageChooser(Composite box, String style)
+                       throws RepositoryException {
+               // FileDialog fileDialog = new FileDialog(getShell());
+               // fileDialog.open();
+               // String fileName = fileDialog.getFileName();
+               CmsImageManager imageManager = CmsUtils.getCmsView().getImageManager();
+               Node node = getNode();
+               JcrFileUploadReceiver receiver = new JcrFileUploadReceiver(
+                               node.getParent(), node.getName() + '[' + node.getIndex() + ']',
+                               imageManager);
+               if (currentUploadHandler != null)
+                       currentUploadHandler.dispose();
+               currentUploadHandler = prepareUpload(receiver);
+               final ServerPushSession pushSession = new ServerPushSession();
+               final FileUpload fileUpload = new FileUpload(box, SWT.NONE);
+               CmsUtils.style(fileUpload, style);
+               fileUpload.addSelectionListener(new SelectionAdapter() {
+                       private static final long serialVersionUID = -9158471843941668562L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               pushSession.start();
+                               fileUpload.submit(currentUploadHandler.getUploadUrl());
+                       }
+               });
+               return fileUpload;
+       }
+
+       protected FileUploadHandler prepareUpload(FileUploadReceiver receiver) {
+               final FileUploadHandler uploadHandler = new FileUploadHandler(receiver);
+               if (fileUploadListener != null)
+                       uploadHandler.addUploadListener(fileUploadListener);
+               return uploadHandler;
+       }
+
+       @Override
+       public Section getSection() {
+               return section;
+       }
+
+       public void setFileUploadListener(FileUploadListener fileUploadListener) {
+               this.fileUploadListener = fileUploadListener;
+               if (currentUploadHandler != null)
+                       currentUploadHandler.addUploadListener(fileUploadListener);
+       }
+
+       @Override
+       public Node getItem() throws RepositoryException {
+               return getNode();
+       }
+
+       @Override
+       public String getPartId() {
+               return getNodeId();
+       }
+
+       @Override
+       public String toString() {
+               return "Img #" + getPartId();
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/Paragraph.java b/org.argeo.cms.ui/src/org/argeo/cms/text/Paragraph.java
new file mode 100644 (file)
index 0000000..a7a7964
--- /dev/null
@@ -0,0 +1,41 @@
+package org.argeo.cms.text;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.Section;
+import org.argeo.cms.viewers.SectionPart;
+import org.argeo.cms.widgets.EditableText;
+
+public class Paragraph extends EditableText implements SectionPart {
+       private static final long serialVersionUID = 3746457776229542887L;
+
+       private final TextSection section;
+
+       public Paragraph(TextSection section, int style, Node node)
+                       throws RepositoryException {
+               super(section, style, node);
+               this.section = section;
+               CmsUtils.style(this, TextStyles.TEXT_PARAGRAPH);
+       }
+
+       public Section getSection() {
+               return section;
+       }
+
+       @Override
+       public String getPartId() {
+               return getNodeId();
+       }
+
+       @Override
+       public Node getItem() throws RepositoryException {
+               return getNode();
+       }
+
+       @Override
+       public String toString() {
+               return "Paragraph #" + getPartId();
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/StandardTextEditor.java b/org.argeo.cms.ui/src/org/argeo/cms/text/StandardTextEditor.java
new file mode 100644 (file)
index 0000000..f39317a
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.cms.text;
+
+import static javax.jcr.Property.JCR_TITLE;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.CmsTypes;
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.ui.internal.text.AbstractTextViewer;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.Section;
+import org.eclipse.swt.widgets.Composite;
+
+/** Text editor where sections and subsections can be managed by the user. */
+public class StandardTextEditor extends AbstractTextViewer {
+       private static final long serialVersionUID = 6049661610883342325L;
+
+       public StandardTextEditor(Composite parent, int style, Node textNode,
+                       CmsEditable cmsEditable) throws RepositoryException {
+               super(new TextSection(parent, style, textNode), style, cmsEditable);
+               refresh();
+               getMainSection().setLayoutData(CmsUtils.fillWidth());
+       }
+
+       @Override
+       protected void initModel(Node textNode) throws RepositoryException {
+               if (isFlat())
+                       textNode.addNode(CMS_P).addMixin(CmsTypes.CMS_STYLED);
+               else
+                       textNode.setProperty(JCR_TITLE, textNode.getName());
+       }
+
+       @Override
+       protected Boolean isModelInitialized(Node textNode)
+                       throws RepositoryException {
+               return textNode.hasProperty(Property.JCR_TITLE)
+                               || textNode.hasNode(CMS_P)
+                               || (!isFlat() && textNode.hasNode(CMS_H));
+       }
+
+       @Override
+       public Section getMainSection() {
+               // TODO Auto-generated method stub
+               return super.getMainSection();
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/TextEditorHeader.java b/org.argeo.cms.ui/src/org/argeo/cms/text/TextEditorHeader.java
new file mode 100644 (file)
index 0000000..5ae0536
--- /dev/null
@@ -0,0 +1,90 @@
+package org.argeo.cms.text;
+
+import java.util.Observable;
+import java.util.Observer;
+
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+
+/** Adds editing capabilities to a page editing text */
+public class TextEditorHeader implements SelectionListener, Observer {
+       private static final long serialVersionUID = 4186756396045701253L;
+
+       private final CmsEditable cmsEditable;
+       private Button publish;
+
+       private Composite parent;
+       private Composite display;
+       private Object layoutData;
+
+       public TextEditorHeader(CmsEditable cmsEditable, Composite parent, int style) {
+               this.cmsEditable = cmsEditable;
+               this.parent = parent;
+               if (this.cmsEditable instanceof Observable)
+                       ((Observable) this.cmsEditable).addObserver(this);
+               refresh();
+       }
+
+       protected void refresh() {
+               if (display != null && !display.isDisposed())
+                       display.dispose();
+               display = null;
+               publish = null;
+               if (cmsEditable.isEditing()) {
+                       display = new Composite(parent, SWT.NONE);
+                       // display.setBackgroundMode(SWT.INHERIT_NONE);
+                       display.setLayoutData(layoutData);
+                       display.setLayout(CmsUtils.noSpaceGridLayout());
+                       CmsUtils.style(display, TextStyles.TEXT_EDITOR_HEADER);
+                       publish = new Button(display, SWT.FLAT | SWT.PUSH);
+                       publish.setText(getPublishButtonLabel());
+                       CmsUtils.style(publish, TextStyles.TEXT_EDITOR_HEADER);
+                       publish.addSelectionListener(this);
+                       display.moveAbove(null);
+               }
+               parent.layout();
+       }
+
+       private String getPublishButtonLabel() {
+               if (cmsEditable.isEditing())
+                       return "Publish";
+               else
+                       return "Edit";
+       }
+
+       @Override
+       public void widgetSelected(SelectionEvent e) {
+               if (e.getSource() == publish) {
+                       if (cmsEditable.isEditing()) {
+                               cmsEditable.stopEditing();
+                       } else {
+                               cmsEditable.startEditing();
+                       }
+                       // publish.setText(getPublishButtonLabel());
+               }
+       }
+
+       @Override
+       public void widgetDefaultSelected(SelectionEvent e) {
+       }
+
+       @Override
+       public void update(Observable o, Object arg) {
+               if (o == cmsEditable) {
+                       // publish.setText(getPublishButtonLabel());
+                       refresh();
+               }
+       }
+
+       public void setLayoutData(Object layoutData) {
+               this.layoutData = layoutData;
+               if (display != null && !display.isDisposed())
+                       display.setLayoutData(layoutData);
+       }
+
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/TextInterpreter.java b/org.argeo.cms.ui/src/org/argeo/cms/text/TextInterpreter.java
new file mode 100644 (file)
index 0000000..f39a2b3
--- /dev/null
@@ -0,0 +1,12 @@
+package org.argeo.cms.text;
+
+import javax.jcr.Item;
+
+/** Convert from/to data layer to/from presentation layer. */
+public interface TextInterpreter {
+       public String raw(Item item);
+
+       public String read(Item item);
+
+       public void write(Item item, String content);
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/TextSection.java b/org.argeo.cms.ui/src/org/argeo/cms/text/TextSection.java
new file mode 100644 (file)
index 0000000..ac93e4b
--- /dev/null
@@ -0,0 +1,52 @@
+package org.argeo.cms.text;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.Section;
+import org.eclipse.swt.widgets.Composite;
+
+public class TextSection extends Section implements CmsNames {
+       private static final long serialVersionUID = -8625209546243220689L;
+       private String defaultTextStyle = TextStyles.TEXT_DEFAULT;
+       private String titleStyle;
+
+       public TextSection(Composite parent, int style, Node node)
+                       throws RepositoryException {
+               this(parent, findSection(parent), style, node);
+       }
+
+       public TextSection(TextSection section, int style, Node node)
+                       throws RepositoryException {
+               this(section, section.getParentSection(), style, node);
+       }
+
+       private TextSection(Composite parent, Section parentSection, int style,
+                       Node node) throws RepositoryException {
+               super(parent, parentSection, style, node);
+               CmsUtils.style(this, TextStyles.TEXT_SECTION);
+       }
+
+       public String getDefaultTextStyle() {
+               return defaultTextStyle;
+       }
+
+       public String getTitleStyle() {
+               if (titleStyle != null)
+                       return titleStyle;
+               // TODO make base H styles configurable
+               Integer relativeDepth = getRelativeDepth();
+               return relativeDepth == 0 ? TextStyles.TEXT_TITLE : TextStyles.TEXT_H
+                               + relativeDepth;
+       }
+
+       public void setDefaultTextStyle(String defaultTextStyle) {
+               this.defaultTextStyle = defaultTextStyle;
+       }
+
+       public void setTitleStyle(String titleStyle) {
+               this.titleStyle = titleStyle;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/TextStyles.java b/org.argeo.cms.ui/src/org/argeo/cms/text/TextStyles.java
new file mode 100644 (file)
index 0000000..44c3ad0
--- /dev/null
@@ -0,0 +1,37 @@
+package org.argeo.cms.text;
+
+/** Styles references in the CSS. */
+public interface TextStyles {
+       /** The whole page area */
+       public final static String TEXT_AREA = "text_area";
+       /** Area providing controls for editing text */
+       public final static String TEXT_EDITOR_HEADER = "text_editor_header";
+       /** The styled composite for editing the text */
+       public final static String TEXT_STYLED_COMPOSITE = "text_styled_composite";
+       /** A section */
+       public final static String TEXT_SECTION = "text_section";
+       /** A paragraph */
+       public final static String TEXT_PARAGRAPH = "text_paragraph";
+       /** An image */
+       public final static String TEXT_IMG = "text_img";
+       /** The dialog to edit styled paragraph */
+       public final static String TEXT_STYLED_TOOLS_DIALOG = "text_styled_tools_dialog";
+
+       /*
+        * DEFAULT TEXT STYLES
+        */
+       /** Default style for text body */
+       public final static String TEXT_DEFAULT = "text_default";
+       /** Fixed-width, typically code */
+       public final static String TEXT_PRE = "text_pre";
+       /** Quote */
+       public final static String TEXT_QUOTE = "text_quote";
+       /** Title */
+       public final static String TEXT_TITLE = "text_title";
+       /** Header (to be dynamically completed with the depth, e.g. text_h1) */
+       public final static String TEXT_H = "text_h";
+
+       /** Default style for images */
+       public final static String TEXT_IMAGE = "text_image";
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/text/WikiPage.java b/org.argeo.cms.ui/src/org/argeo/cms/text/WikiPage.java
new file mode 100644 (file)
index 0000000..a01cc35
--- /dev/null
@@ -0,0 +1,67 @@
+package org.argeo.cms.text;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.CmsTypes;
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.cms.util.CmsLink;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.JcrVersionCmsEditable;
+import org.argeo.cms.widgets.ScrolledPage;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/** Display the text of the context, and provide an editor if the user can edit. */
+public class WikiPage implements CmsUiProvider, CmsNames {
+       @Override
+       public Control createUi(Composite parent, Node context)
+                       throws RepositoryException {
+               CmsEditable cmsEditable = new JcrVersionCmsEditable(context);
+               if (cmsEditable.canEdit())
+                       new TextEditorHeader(cmsEditable, parent, SWT.NONE)
+                                       .setLayoutData(CmsUtils.fillWidth());
+
+               ScrolledPage page = new ScrolledPage(parent, SWT.NONE);
+               page.setLayout(CmsUtils.noSpaceGridLayout());
+               GridData textGd = CmsUtils.fillAll();
+               page.setLayoutData(textGd);
+
+               if (context.isNodeType(CmsTypes.CMS_TEXT)) {
+                       new StandardTextEditor(page, SWT.NONE, context, cmsEditable);
+               } else if (context.isNodeType(NodeType.NT_FOLDER)
+                               || context.getPath().equals("/")) {
+                       parent.setBackgroundMode(SWT.INHERIT_NONE);
+                       if (context.getSession().hasPermission(context.getPath(),
+                                       Session.ACTION_ADD_NODE)) {
+                               Node indexNode = JcrUtils.getOrAdd(context, CMS_INDEX,
+                                               CmsTypes.CMS_TEXT);
+                               new StandardTextEditor(page, SWT.NONE, indexNode, cmsEditable);
+                               textGd.heightHint = 400;
+
+                               for (NodeIterator ni = context.getNodes(); ni.hasNext();) {
+                                       Node textNode = ni.nextNode();
+                                       if (textNode.isNodeType(NodeType.NT_FOLDER))
+                                               new CmsLink(textNode.getName() + "/",
+                                                               textNode.getPath()).createUi(parent, textNode);
+                               }
+                               for (NodeIterator ni = context.getNodes(); ni.hasNext();) {
+                                       Node textNode = ni.nextNode();
+                                       if (textNode.isNodeType(CmsTypes.CMS_TEXT)
+                                                       && !textNode.getName().equals(CMS_INDEX))
+                                               new CmsLink(textNode.getName(), textNode.getPath())
+                                                               .createUi(parent, textNode);
+                               }
+                       }
+               }
+               return page;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/AbstractCmsEntryPoint.java
new file mode 100644 (file)
index 0000000..529061e
--- /dev/null
@@ -0,0 +1,387 @@
+package org.argeo.cms.ui;
+
+import static org.argeo.naming.SharedSecret.X_SHARED_SECRET;
+
+import java.io.IOException;
+import java.security.PrivilegedAction;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.PathNotFoundException;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.auth.HttpRequestCallback;
+import org.argeo.cms.auth.HttpRequestCallbackHandler;
+import org.argeo.eclipse.ui.specific.UiContext;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.naming.AuthPassword;
+import org.argeo.naming.SharedSecret;
+import org.argeo.node.NodeConstants;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.application.AbstractEntryPoint;
+import org.eclipse.rap.rwt.client.WebClient;
+import org.eclipse.rap.rwt.client.service.BrowserNavigation;
+import org.eclipse.rap.rwt.client.service.BrowserNavigationEvent;
+import org.eclipse.rap.rwt.client.service.BrowserNavigationListener;
+import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+/** Manages history and navigation */
+public abstract class AbstractCmsEntryPoint extends AbstractEntryPoint implements CmsView {
+       private static final long serialVersionUID = 906558779562569784L;
+
+       private final Log log = LogFactory.getLog(AbstractCmsEntryPoint.class);
+
+       // private final Subject subject;
+       private LoginContext loginContext;
+
+       private final Repository repository;
+       private final String workspace;
+       private final String defaultPath;
+       private final Map<String, String> factoryProperties;
+
+       // Current state
+       private Session session;
+       private Node node;
+       private String nodePath;// useful when changing auth
+       private String state;
+       private Throwable exception;
+
+       // Client services
+       private final JavaScriptExecutor jsExecutor;
+       private final BrowserNavigation browserNavigation;
+
+       public AbstractCmsEntryPoint(Repository repository, String workspace, String defaultPath,
+                       Map<String, String> factoryProperties) {
+               this.repository = repository;
+               this.workspace = workspace;
+               this.defaultPath = defaultPath;
+               this.factoryProperties = new HashMap<String, String>(factoryProperties);
+               // subject = new Subject();
+
+               // Initial login
+               LoginContext lc;
+               try {
+                       lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
+                                       new HttpRequestCallbackHandler(UiContext.getHttpRequest(), UiContext.getHttpResponse()));
+                       lc.login();
+               } catch (LoginException e) {
+                       try {
+                               lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS);
+                               lc.login();
+                       } catch (LoginException e1) {
+                               throw new CmsException("Cannot log in as anonymous", e1);
+                       }
+               }
+               authChange(lc);
+
+               jsExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
+               browserNavigation = RWT.getClient().getService(BrowserNavigation.class);
+               if (browserNavigation != null)
+                       browserNavigation.addBrowserNavigationListener(new CmsNavigationListener());
+       }
+
+       @Override
+       protected Shell createShell(Display display) {
+               Shell shell = super.createShell(display);
+               shell.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_SHELL);
+               display.disposeExec(new Runnable() {
+
+                       @Override
+                       public void run() {
+                               if (log.isTraceEnabled())
+                                       log.trace("Logging out " + session);
+                               JcrUtils.logoutQuietly(session);
+                       }
+               });
+               return shell;
+       }
+
+       @Override
+       protected final void createContents(final Composite parent) {
+               UiContext.setData(CmsView.KEY, this);
+               Subject.doAs(getSubject(), new PrivilegedAction<Void>() {
+                       @Override
+                       public Void run() {
+                               try {
+                                       initUi(parent);
+                               } catch (Exception e) {
+                                       throw new CmsException("Cannot create entrypoint contents", e);
+                               }
+                               return null;
+                       }
+               });
+       }
+
+       /** Create UI */
+       protected abstract void initUi(Composite parent);
+
+       /** Recreate UI after navigation or auth change */
+       protected abstract void refresh();
+
+       /**
+        * The node to return when no node was found (for authenticated users and
+        * anonymous)
+        */
+//     private Node getDefaultNode(Session session) throws RepositoryException {
+//             if (!session.hasPermission(defaultPath, "read")) {
+//                     String userId = session.getUserID();
+//                     if (userId.equals(NodeConstants.ROLE_ANONYMOUS))
+//                             // TODO throw a special exception
+//                             throw new CmsException("Login required");
+//                     else
+//                             throw new CmsException("Unauthorized");
+//             }
+//             return session.getNode(defaultPath);
+//     }
+
+       protected String getBaseTitle() {
+               return factoryProperties.get(WebClient.PAGE_TITLE);
+       }
+
+       public void navigateTo(String state) {
+               exception = null;
+               String title = setState(state);
+               doRefresh();
+               if (browserNavigation != null)
+                       browserNavigation.pushState(state, title);
+       }
+
+       // @Override
+       // public synchronized Subject getSubject() {
+       // return subject;
+       // }
+
+       // @Override
+       // public LoginContext getLoginContext() {
+       // return loginContext;
+       // }
+       protected Subject getSubject() {
+               return loginContext.getSubject();
+       }
+
+       @Override
+       public boolean isAnonymous() {
+               return CurrentUser.isAnonymous(getSubject());
+       }
+
+       @Override
+       public synchronized void logout() {
+               if (loginContext == null)
+                       throw new CmsException("Login context should not be null");
+               try {
+                       CurrentUser.logoutCmsSession(loginContext.getSubject());
+                       loginContext.logout();
+                       LoginContext anonymousLc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS);
+                       anonymousLc.login();
+                       authChange(anonymousLc);
+               } catch (LoginException e) {
+                       log.error("Cannot logout", e);
+               }
+       }
+
+       @Override
+       public synchronized void authChange(LoginContext lc) {
+               if (lc == null)
+                       throw new CmsException("Login context cannot be null");
+               // logout previous login context
+               if (this.loginContext != null)
+                       try {
+                               this.loginContext.logout();
+                       } catch (LoginException e1) {
+                               log.warn("Could not log out: " + e1);
+                       }
+               this.loginContext = lc;
+               Subject.doAs(getSubject(), new PrivilegedAction<Void>() {
+
+                       @Override
+                       public Void run() {
+                               try {
+                                       JcrUtils.logoutQuietly(session);
+                                       session = repository.login(workspace);
+                                       if (nodePath != null)
+                                               try {
+                                                       node = session.getNode(nodePath);
+                                               } catch (PathNotFoundException e) {
+                                                       navigateTo("~");
+                                               }
+
+                                       // refresh UI
+                                       doRefresh();
+                               } catch (RepositoryException e) {
+                                       throw new CmsException("Cannot perform auth change", e);
+                               }
+                               return null;
+                       }
+
+               });
+       }
+
+       @Override
+       public void exception(final Throwable e) {
+               AbstractCmsEntryPoint.this.exception = e;
+               log.error("Unexpected exception in CMS", e);
+               doRefresh();
+       }
+
+       protected synchronized void doRefresh() {
+               Subject.doAs(getSubject(), new PrivilegedAction<Void>() {
+                       @Override
+                       public Void run() {
+                               refresh();
+                               return null;
+                       }
+               });
+       }
+
+       /** Sets the state of the entry point and retrieve the related JCR node. */
+       protected synchronized String setState(String newState) {
+               String previousState = this.state;
+
+               String newNodePath = null;
+               String prefix = null;
+               this.state = newState;
+               if (newState.equals("~"))
+                       this.state = "";
+
+               try {
+                       int firstSlash = state.indexOf('/');
+                       if (firstSlash == 0) {
+                               newNodePath = state;
+                               prefix = "";
+                       } else if (firstSlash > 0) {
+                               prefix = state.substring(0, firstSlash);
+                               newNodePath = state.substring(firstSlash);
+                       } else {
+                               newNodePath = defaultPath;
+                               prefix = state;
+
+                       }
+
+                       // auth
+                       int colonIndex = prefix.indexOf('$');
+                       if (colonIndex > 0) {
+                               SharedSecret token = new SharedSecret(new AuthPassword(X_SHARED_SECRET + '$' + prefix)) {
+
+                                       @Override
+                                       public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+                                               super.handle(callbacks);
+                                               // handle HTTP context
+                                               for (Callback callback : callbacks) {
+                                                       if (callback instanceof HttpRequestCallback) {
+                                                               ((HttpRequestCallback) callback).setRequest(UiContext.getHttpRequest());
+                                                               ((HttpRequestCallback) callback).setResponse(UiContext.getHttpResponse());
+                                                       }
+                                               }
+                                       }
+                               };
+                               LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, token);
+                               lc.login();
+                               authChange(lc);// sets the node as well
+                               // } else {
+                               // // TODO check consistency
+                               // }
+                       } else {
+                               Node newNode = null;
+                               if (session.nodeExists(newNodePath))
+                                       newNode = session.getNode(newNodePath);
+                               else {
+//                                     throw new CmsException("Data " + newNodePath + " does not exist");
+                                       newNode = null;
+                               }
+                               setNode(newNode);
+                       }
+                       String title = publishMetaData(getNode());
+
+                       if (log.isTraceEnabled())
+                               log.trace("node=" + newNodePath + ", state=" + state + " (prefix=" + prefix + ")");
+
+                       return title;
+               } catch (Exception e) {
+                       log.error("Cannot set state '" + state + "'", e);
+                       if (state.equals("") || newState.equals("~") || newState.equals(previousState))
+                               return "Unrecoverable exception : " + e.getClass().getSimpleName();
+                       if (previousState.equals(""))
+                               previousState = "~";
+                       navigateTo(previousState);
+                       throw new CmsException("Unexpected issue when accessing #" + newState, e);
+               }
+       }
+
+       private String publishMetaData(Node node) throws RepositoryException {
+               // Title
+               String title;
+               if (node!=null && node.isNodeType(NodeType.MIX_TITLE) && node.hasProperty(Property.JCR_TITLE))
+                       title = node.getProperty(Property.JCR_TITLE).getString() + " - " + getBaseTitle();
+               else
+                       title = getBaseTitle();
+
+               HttpServletRequest request = UiContext.getHttpRequest();
+               if (request == null)
+                       return null;
+
+               StringBuilder js = new StringBuilder();
+               title = title.replace("'", "\\'");// sanitize
+               js.append("document.title = '" + title + "';");
+               jsExecutor.execute(js.toString());
+               return title;
+       }
+
+       // Simply remove some illegal character
+       // private String clean(String stringToClean) {
+       // return stringToClean.replaceAll("'", "").replaceAll("\\n", "")
+       // .replaceAll("\\t", "");
+       // }
+
+       protected synchronized Node getNode() {
+               return node;
+       }
+
+       private synchronized void setNode(Node node) throws RepositoryException {
+               this.node = node;
+               this.nodePath = node == null ? null : node.getPath();
+       }
+
+       protected String getState() {
+               return state;
+       }
+
+       protected Throwable getException() {
+               return exception;
+       }
+
+       protected void resetException() {
+               exception = null;
+       }
+
+       protected Session getSession() {
+               return session;
+       }
+
+       private class CmsNavigationListener implements BrowserNavigationListener {
+               private static final long serialVersionUID = -3591018803430389270L;
+
+               @Override
+               public void navigated(BrowserNavigationEvent event) {
+                       setState(event.getState());
+                       doRefresh();
+               }
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsConstants.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsConstants.java
new file mode 100644 (file)
index 0000000..53e2aee
--- /dev/null
@@ -0,0 +1,24 @@
+package org.argeo.cms.ui;
+
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.graphics.Point;
+
+/** Commons constants */
+public interface CmsConstants {
+       // DATAKEYS
+       public final static String STYLE = RWT.CUSTOM_VARIANT;
+       public final static String MARKUP = RWT.MARKUP_ENABLED;
+       public final static String ITEM_HEIGHT = RWT.CUSTOM_ITEM_HEIGHT;
+
+       // EVENT DETAILS
+       public final static int HYPERLINK = RWT.HYPERLINK;
+
+       // STANDARD RESOURCES
+       public final static String LOADING_IMAGE = "icons/loading.gif";
+
+       public final static String NO_IMAGE = "icons/noPic-square-640px.png";
+       public final static Point NO_IMAGE_SIZE = new Point(640, 640);
+       public final static Float NO_IMAGE_RATIO = 1f;
+       // MISCEALLENEOUS
+       String DATE_TIME_FORMAT = "dd/MM/yyyy, HH:mm";
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsEditable.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsEditable.java
new file mode 100644 (file)
index 0000000..687e3e8
--- /dev/null
@@ -0,0 +1,36 @@
+package org.argeo.cms.ui;
+
+/** API NOT STABLE (yet). */
+public interface CmsEditable {
+
+       /** Whether the calling thread can edit, the value is immutable */
+       public Boolean canEdit();
+
+       public Boolean isEditing();
+
+       public void startEditing();
+
+       public void stopEditing();
+
+       public static CmsEditable NON_EDITABLE = new CmsEditable() {
+
+               @Override
+               public void stopEditing() {
+               }
+
+               @Override
+               public void startEditing() {
+               }
+
+               @Override
+               public Boolean isEditing() {
+                       return false;
+               }
+
+               @Override
+               public Boolean canEdit() {
+                       return false;
+               }
+       };
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsEditionEvent.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsEditionEvent.java
new file mode 100644 (file)
index 0000000..872142b
--- /dev/null
@@ -0,0 +1,23 @@
+package org.argeo.cms.ui;
+
+import java.util.EventObject;
+
+/** Notify of the edition lifecycle */
+public class CmsEditionEvent extends EventObject {
+       private static final long serialVersionUID = 950914736016693110L;
+
+       public final static Integer START_EDITING = 0;
+       public final static Integer STOP_EDITING = 1;
+
+       private final Integer type;
+
+       public CmsEditionEvent(Object source, Integer type) {
+               super(source);
+               this.type = type;
+       }
+
+       public Integer getType() {
+               return type;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsImageManager.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsImageManager.java
new file mode 100644 (file)
index 0000000..eb9cb75
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.cms.ui;
+
+import java.io.InputStream;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Control;
+
+/** Read and write access to images. */
+public interface CmsImageManager {
+       /** Load image in control */
+       public Boolean load(Node node, Control control, Point size) throws RepositoryException;
+
+       /** @return (0,0) if not available */
+       public Point getImageSize(Node node) throws RepositoryException;
+
+       /**
+        * The related &lt;img&gt; tag, with src, width and height set.
+        * 
+        * @return null if not available
+        */
+       public String getImageTag(Node node) throws RepositoryException;
+
+       /**
+        * The related &lt;img&gt; tag, with url, width and height set. Caller must
+        * close the tag (or add additional attributes).
+        * 
+        * @return null if not available
+        */
+       public StringBuilder getImageTagBuilder(Node node, Point size) throws RepositoryException;
+
+       /**
+        * Returns the remotely accessible URL of the image (registering it if
+        * needed) @return null if not available
+        */
+       public String getImageUrl(Node node) throws RepositoryException;
+
+       public Binary getImageBinary(Node node) throws RepositoryException;
+
+       public Image getSwtImage(Node node) throws RepositoryException;
+
+       /** @return URL */
+       public String uploadImage(Node parentNode, String fileName, InputStream in) throws RepositoryException;
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsStyles.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsStyles.java
new file mode 100644 (file)
index 0000000..506e971
--- /dev/null
@@ -0,0 +1,31 @@
+package org.argeo.cms.ui;
+
+/** Styles references in the CSS. */
+public interface CmsStyles {
+       // General
+       public final static String CMS_SHELL = "cms_shell";
+       public final static String CMS_MENU_LINK = "cms_menu_link";
+
+       // Header
+       public final static String CMS_HEADER = "cms_header";
+       public final static String CMS_HEADER_LEAD = "cms_header-lead";
+       public final static String CMS_HEADER_CENTER = "cms_header-center";
+       public final static String CMS_HEADER_END = "cms_header-end";
+
+       public final static String CMS_LEAD = "cms_lead";
+       public final static String CMS_END = "cms_end";
+       public final static String CMS_FOOTER = "cms_footer";
+
+       public final static String CMS_USER_MENU = "cms_user_menu";
+       public final static String CMS_USER_MENU_LINK = "cms_user_menu-link";
+       public final static String CMS_USER_MENU_ITEM = "cms_user_menu-item";
+       public final static String CMS_LOGIN_DIALOG = "cms_login_dialog";
+       public final static String CMS_LOGIN_DIALOG_USERNAME = "cms_login_dialog-username";
+       public final static String CMS_LOGIN_DIALOG_PASSWORD = "cms_login_dialog-password";
+
+       // Body
+       public final static String CMS_SCROLLED_AREA = "cms_scrolled_area";
+       public final static String CMS_BODY = "cms_body";
+       public final static String CMS_STATIC_TEXT = "cms_static-text";
+       public final static String CMS_LINK = "cms_link";
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsUiProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsUiProvider.java
new file mode 100644 (file)
index 0000000..24415c8
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.cms.ui;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/** Stateless factory building an SWT user interface given a JCR context. */
+@FunctionalInterface
+public interface CmsUiProvider {
+       /**
+        * Initialises a user interface.
+        * 
+        * @param parent
+        *            the parent composite
+        * @param context
+        *            a context node (holding the JCR underlying session), or null
+        */
+       public Control createUi(Composite parent, Node context) throws RepositoryException;
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsView.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/CmsView.java
new file mode 100644 (file)
index 0000000..6d70935
--- /dev/null
@@ -0,0 +1,27 @@
+package org.argeo.cms.ui;
+
+import javax.security.auth.login.LoginContext;
+
+/** Provides interaction with the CMS system. */
+public interface CmsView {
+       String KEY = "org.argeo.cms.ui.view";
+
+       UxContext getUxContext();
+
+       // NAVIGATION
+       void navigateTo(String state);
+
+       // SECURITY
+       void authChange(LoginContext loginContext);
+
+       void logout();
+
+       // void registerCallbackHandler(CallbackHandler callbackHandler);
+
+       // SERVICES
+       void exception(Throwable e);
+
+       CmsImageManager getImageManager();
+
+       boolean isAnonymous();
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/LifeCycleUiProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/LifeCycleUiProvider.java
new file mode 100644 (file)
index 0000000..5d77c15
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.cms.ui;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+/** CmsUiProvider notified of initialisation with a system session. */
+public interface LifeCycleUiProvider extends CmsUiProvider {
+       public void init(Session adminSession) throws RepositoryException;
+
+       public void destroy();
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/UxContext.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/UxContext.java
new file mode 100644 (file)
index 0000000..42d7ab3
--- /dev/null
@@ -0,0 +1,18 @@
+package org.argeo.cms.ui;
+
+public interface UxContext {
+       boolean isPortrait();
+
+       boolean isLandscape();
+
+       boolean isSquare();
+
+       boolean isSmall();
+
+       /**
+        * Is a production environment (must be false by default, and be explicitly
+        * set during the CMS deployment). When false, it can activate additional UI
+        * capabilities in order to facilitate QA.
+        */
+       boolean isMasterData();
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsFeedback.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsFeedback.java
new file mode 100644 (file)
index 0000000..3ee0833
--- /dev/null
@@ -0,0 +1,97 @@
+package org.argeo.cms.ui.dialogs;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsMsg;
+import org.argeo.eclipse.ui.Selected;
+import org.argeo.eclipse.ui.dialogs.LightweightDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+public class CmsFeedback extends LightweightDialog {
+       private final static Log log = LogFactory.getLog(CmsFeedback.class);
+
+       private String message;
+       private Throwable exception;
+
+       public CmsFeedback(Shell parentShell, String message, Throwable e) {
+               super(parentShell);
+               this.message = message;
+               this.exception = e;
+               log.error(message, e);
+       }
+
+       public static void show(String message, Throwable e) {
+               // rethrow ThreaDeath in order to make sure that RAP will properly clean
+               // up the UI thread
+               if (e instanceof ThreadDeath)
+                       throw (ThreadDeath) e;
+
+               try {
+                       CmsFeedback cmsFeedback = new CmsFeedback(null, message, e);
+                       cmsFeedback.setBlockOnOpen(false);
+                       cmsFeedback.open();
+               } catch (Throwable e1) {
+                       log.error("Cannot open error feedback (" + e.getMessage() + "), original error below", e);
+               }
+       }
+
+       public static void show(String message) {
+               new CmsFeedback(null, message, null).open();
+       }
+
+       /** Tries to find a display */
+       // private static Display getDisplay() {
+       // try {
+       // Display display = Display.getCurrent();
+       // if (display != null)
+       // return display;
+       // else
+       // return Display.getDefault();
+       // } catch (Exception e) {
+       // return Display.getCurrent();
+       // }
+       // }
+
+       protected Control createDialogArea(Composite parent) {
+               parent.setLayout(new GridLayout(2, false));
+
+               Label messageLbl = new Label(parent, SWT.WRAP);
+               if (message != null)
+                       messageLbl.setText(message);
+               else if (exception != null)
+                       messageLbl.setText(exception.getLocalizedMessage());
+
+               Button close = new Button(parent, SWT.FLAT);
+               close.setText(CmsMsg.close.lead());
+               close.setLayoutData(new GridData(SWT.END, SWT.TOP, false, false));
+               close.addSelectionListener((Selected) (e) -> closeShell(OK));
+
+               // Composite composite = new Composite(dialogarea, SWT.NONE);
+               // composite.setLayout(new GridLayout(2, false));
+               // composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               if (exception != null) {
+                       Text stack = new Text(parent, SWT.MULTI | SWT.LEAD | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+                       stack.setEditable(false);
+                       stack.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
+                       StringWriter sw = new StringWriter();
+                       exception.printStackTrace(new PrintWriter(sw));
+                       stack.setText(sw.toString());
+               }
+
+               // parent.pack();
+               return messageLbl;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsMessageDialog.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsMessageDialog.java
new file mode 100644 (file)
index 0000000..21d4792
--- /dev/null
@@ -0,0 +1,115 @@
+package org.argeo.cms.ui.dialogs;
+
+import org.argeo.cms.CmsMsg;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.Selected;
+import org.argeo.eclipse.ui.dialogs.LightweightDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+public class CmsMessageDialog extends LightweightDialog {
+       public final static int INFORMATION = 2;
+       public final static int QUESTION = 3;
+       public final static int WARNING = 4;
+       public final static int CONFIRM = 5;
+
+       private int kind;
+       private String message;
+
+       public CmsMessageDialog(Shell parentShell, String message, int kind) {
+               super(parentShell);
+               this.kind = kind;
+               this.message = message;
+       }
+
+       protected Control createDialogArea(Composite parent) {
+               parent.setLayout(new GridLayout());
+
+               // message
+               Composite body = new Composite(parent, SWT.NONE);
+               GridLayout bodyGridLayout = new GridLayout();
+               bodyGridLayout.marginHeight = 20;
+               bodyGridLayout.marginWidth = 20;
+               body.setLayout(bodyGridLayout);
+               body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               Label messageLbl = new Label(body, SWT.WRAP);
+               messageLbl.setFont(EclipseUiUtils.getBoldFont(parent));
+               if (message != null)
+                       messageLbl.setText(message);
+
+               // buttons
+               Composite buttons = new Composite(parent, SWT.NONE);
+               buttons.setLayoutData(new GridData(SWT.END, SWT.FILL, true, false));
+               if (kind == INFORMATION || kind == WARNING) {
+                       GridLayout layout = new GridLayout(1, true);
+                       layout.marginWidth = 0;
+                       layout.marginHeight = 0;
+                       buttons.setLayout(layout);
+
+                       Button close = new Button(buttons, SWT.FLAT);
+                       close.setText(CmsMsg.close.lead());
+                       close.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+                       close.addSelectionListener((Selected) (e) -> closeShell(OK));
+               } else if (kind == CONFIRM || kind == QUESTION) {
+                       GridLayout layout = new GridLayout(2, true);
+                       layout.marginWidth = 0;
+                       layout.marginHeight = 0;
+                       buttons.setLayout(layout);
+
+                       Button cancel = new Button(buttons, SWT.FLAT);
+                       cancel.setText(CmsMsg.cancel.lead());
+                       cancel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+                       cancel.addSelectionListener((Selected) (e) -> cancelPressed());
+
+                       Button ok = new Button(buttons, SWT.FLAT);
+                       ok.setText(CmsMsg.ok.lead());
+                       ok.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+                       ok.addSelectionListener((Selected) (e) -> okPressed());
+               }
+               // pack();
+               return body;
+       }
+
+       protected void okPressed() {
+               closeShell(OK);
+       }
+
+       protected void cancelPressed() {
+               closeShell(CANCEL);
+       }
+
+       protected Point getInitialSize() {
+               return new Point(400, 200);
+       }
+
+       public static boolean open(int kind, Shell parent, String message) {
+               CmsMessageDialog dialog = new CmsMessageDialog(parent, message, kind);
+               return dialog.open() == 0;
+       }
+
+       public static boolean openConfirm(String message) {
+               return open(CONFIRM, Display.getCurrent().getActiveShell(), message);
+       }
+
+       public static void openInformation(String message) {
+               open(INFORMATION, Display.getCurrent().getActiveShell(), message);
+       }
+
+       public static boolean openQuestion(String message) {
+               return open(QUESTION, Display.getCurrent().getActiveShell(), message);
+       }
+
+       public static void openWarning(String message) {
+               open(WARNING, Display.getCurrent().getActiveShell(), message);
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsWizardDialog.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsWizardDialog.java
new file mode 100644 (file)
index 0000000..de41bbf
--- /dev/null
@@ -0,0 +1,221 @@
+package org.argeo.cms.ui.dialogs;
+
+import java.lang.reflect.InvocationTargetException;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.CmsMsg;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.Selected;
+import org.argeo.eclipse.ui.dialogs.LightweightDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.wizard.IWizard;
+import org.eclipse.jface.wizard.IWizardContainer2;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+public class CmsWizardDialog extends LightweightDialog implements IWizardContainer2 {
+       private static final long serialVersionUID = -2123153353654812154L;
+
+       private IWizard wizard;
+       private IWizardPage currentPage;
+       private int currentPageIndex;
+
+       private Label titleBar;
+       private Label message;
+       private Composite[] pageBodies;
+       private Composite buttons;
+       private Button back;
+       private Button next;
+       private Button finish;
+
+       public CmsWizardDialog(Shell parentShell, IWizard wizard) {
+               super(parentShell);
+               this.wizard = wizard;
+               wizard.setContainer(this);
+               // create the pages
+               wizard.addPages();
+               currentPage = wizard.getStartingPage();
+               if (currentPage == null)
+                       throw new CmsException("At least one wizard page is required");
+       }
+
+       @Override
+       protected Control createDialogArea(Composite parent) {
+               updateWindowTitle();
+
+               Composite messageArea = new Composite(parent, SWT.NONE);
+               messageArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               {
+                       messageArea.setLayout(CmsUtils.noSpaceGridLayout(new GridLayout(2, false)));
+                       titleBar = new Label(messageArea, SWT.WRAP);
+                       titleBar.setFont(EclipseUiUtils.getBoldFont(parent));
+                       titleBar.setLayoutData(new GridData(SWT.BEGINNING, SWT.FILL, true, false));
+                       updateTitleBar();
+                       Button cancelButton = new Button(messageArea, SWT.FLAT);
+                       cancelButton.setText(CmsMsg.cancel.lead());
+                       cancelButton.setLayoutData(new GridData(SWT.END, SWT.TOP, false, false, 1, 3));
+                       cancelButton.addSelectionListener((Selected) (e) -> closeShell(CANCEL));
+                       message = new Label(messageArea, SWT.WRAP);
+                       message.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 1, 2));
+                       updateMessage();
+               }
+
+               Composite body = new Composite(parent, SWT.BORDER);
+               body.setLayout(new FormLayout());
+               body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               pageBodies = new Composite[wizard.getPageCount()];
+               IWizardPage[] pages = wizard.getPages();
+               for (int i = 0; i < pages.length; i++) {
+                       pageBodies[i] = new Composite(body, SWT.NONE);
+                       pageBodies[i].setLayout(CmsUtils.noSpaceGridLayout());
+                       setSwitchingFormData(pageBodies[i]);
+                       pages[i].createControl(pageBodies[i]);
+               }
+               showPage(currentPage);
+
+               buttons = new Composite(parent, SWT.NONE);
+               buttons.setLayoutData(new GridData(SWT.END, SWT.FILL, true, false));
+               {
+                       boolean singlePage = wizard.getPageCount() == 1;
+                       // singlePage = false;// dev
+                       GridLayout layout = new GridLayout(singlePage ? 1 : 3, true);
+                       layout.marginWidth = 0;
+                       layout.marginHeight = 0;
+                       buttons.setLayout(layout);
+                       // TODO revert order for right-to-left languages
+
+                       if (!singlePage) {
+                               back = new Button(buttons, SWT.PUSH);
+                               back.setText(CmsMsg.wizardBack.lead());
+                               back.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+                               back.addSelectionListener((Selected) (e) -> backPressed());
+
+                               next = new Button(buttons, SWT.PUSH);
+                               next.setText(CmsMsg.wizardNext.lead());
+                               next.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+                               next.addSelectionListener((Selected) (e) -> nextPressed());
+                       }
+                       finish = new Button(buttons, SWT.PUSH);
+                       finish.setText(CmsMsg.wizardFinish.lead());
+                       finish.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+                       finish.addSelectionListener((Selected) (e) -> finishPressed());
+
+                       updateButtons();
+               }
+               return body;
+       }
+
+       @Override
+       public IWizardPage getCurrentPage() {
+               return currentPage;
+       }
+
+       @Override
+       public Shell getShell() {
+               return getForegoundShell();
+       }
+
+       @Override
+       public void showPage(IWizardPage page) {
+               IWizardPage[] pages = wizard.getPages();
+               int index = -1;
+               for (int i = 0; i < pages.length; i++) {
+                       if (page == pages[i]) {
+                               index = i;
+                               break;
+                       }
+               }
+               if (index < 0)
+                       throw new CmsException("Cannot find index of wizard page " + page);
+               pageBodies[index].moveAbove(pageBodies[currentPageIndex]);
+
+               // // clear
+               // for (Control c : body.getChildren())
+               // c.dispose();
+               // page.createControl(body);
+               // body.layout(true, true);
+               currentPageIndex = index;
+               currentPage = page;
+       }
+
+       @Override
+       public void updateButtons() {
+               if (back != null)
+                       back.setEnabled(wizard.getPreviousPage(currentPage) != null);
+               if (next != null)
+                       next.setEnabled(wizard.getNextPage(currentPage) != null && currentPage.canFlipToNextPage());
+               if (finish != null) {
+                       finish.setEnabled(wizard.canFinish());
+               }
+       }
+
+       @Override
+       public void updateMessage() {
+               if (currentPage.getMessage() != null)
+                       message.setText(currentPage.getMessage());
+       }
+
+       @Override
+       public void updateTitleBar() {
+               if (currentPage.getTitle() != null)
+                       titleBar.setText(currentPage.getTitle());
+       }
+
+       @Override
+       public void updateWindowTitle() {
+               setTitle(wizard.getWindowTitle());
+       }
+
+       @Override
+       public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable)
+                       throws InvocationTargetException, InterruptedException {
+               runnable.run(null);
+       }
+
+       @Override
+       public void updateSize() {
+               // TODO pack?
+       }
+
+       protected boolean onCancel() {
+               return wizard.performCancel();
+       }
+
+       protected void nextPressed() {
+               IWizardPage page = wizard.getNextPage(currentPage);
+               showPage(page);
+               updateButtons();
+       }
+
+       protected void backPressed() {
+               IWizardPage page = wizard.getPreviousPage(currentPage);
+               showPage(page);
+               updateButtons();
+       }
+
+       protected void finishPressed() {
+               if (wizard.performFinish())
+                       closeShell(OK);
+       }
+
+       private static void setSwitchingFormData(Composite composite) {
+               FormData fdLabel = new FormData();
+               fdLabel.top = new FormAttachment(0, 0);
+               fdLabel.left = new FormAttachment(0, 0);
+               fdLabel.right = new FormAttachment(100, 0);
+               fdLabel.bottom = new FormAttachment(100, 0);
+               composite.setLayoutData(fdLabel);
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java
new file mode 100644 (file)
index 0000000..4ce4688
--- /dev/null
@@ -0,0 +1,108 @@
+package org.argeo.cms.ui.eclipse.forms;
+/**
+ * AbstractFormPart implements IFormPart interface and can be used as a
+ * convenient base class for concrete form parts. If a method contains
+ * code that must be called, look for instructions to call 'super'
+ * when overriding.
+ * 
+ * @see org.eclipse.ui.forms.widgets.Section
+ * @since 1.0
+ */
+public abstract class AbstractFormPart implements IFormPart {
+       private IManagedForm managedForm;
+       private boolean dirty = false;
+       private boolean stale = true;
+       /**
+        * @see org.eclipse.ui.forms.IFormPart#initialize(org.eclipse.ui.forms.IManagedForm)
+        */
+       public void initialize(IManagedForm form) {
+               this.managedForm = form;
+       }
+       /**
+        * Returns the form that manages this part.
+        * 
+        * @return the managed form
+        */
+       public IManagedForm getManagedForm() {
+               return managedForm;
+       }
+       /**
+        * Disposes the part. Subclasses should override to release any system
+        * resources.
+        */
+       public void dispose() {
+       }
+       /**
+        * Commits the part. Subclasses should call 'super' when overriding.
+        * 
+        * @param onSave
+        *            <code>true</code> if the request to commit has arrived as a
+        *            result of the 'save' action.
+        */
+       public void commit(boolean onSave) {
+               dirty = false;
+       }
+       /**
+        * Sets the overall form input. Subclases may elect to override the method
+        * and adjust according to the form input.
+        * 
+        * @param input
+        *            the form input object
+        * @return <code>false</code>
+        */
+       public boolean setFormInput(Object input) {
+               return false;
+       }
+       /**
+        * Instructs the part to grab keyboard focus.
+        */
+       public void setFocus() {
+       }
+       /**
+        * Refreshes the section after becoming stale (falling behind data in the
+        * model). Subclasses must call 'super' when overriding this method.
+        */
+       public void refresh() {
+               stale = false;
+               // since we have refreshed, any changes we had in the
+               // part are gone and we are not dirty
+               dirty = false;
+       }
+       /**
+        * Marks the part dirty. Subclasses should call this method as a result of
+        * user interaction with the widgets in the section.
+        */
+       public void markDirty() {
+               dirty = true;
+               managedForm.dirtyStateChanged();
+       }
+       /**
+        * Tests whether the part is dirty i.e. its widgets have state that is
+        * newer than the data in the model.
+        * 
+        * @return <code>true</code> if the part is dirty, <code>false</code>
+        *         otherwise.
+        */
+       public boolean isDirty() {
+               return dirty;
+       }
+       /**
+        * Tests whether the part is stale i.e. its widgets have state that is
+        * older than the data in the model.
+        * 
+        * @return <code>true</code> if the part is stale, <code>false</code>
+        *         otherwise.
+        */
+       public boolean isStale() {
+               return stale;
+       }
+       /**
+        * Marks the part stale. Subclasses should call this method as a result of
+        * model notification that indicates that the content of the section is no
+        * longer in sync with the model.
+        */
+       public void markStale() {
+               stale = true;
+               managedForm.staleStateChanged();
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormColors.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormColors.java
new file mode 100644 (file)
index 0000000..32b031b
--- /dev/null
@@ -0,0 +1,730 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.RGB;
+//import org.eclipse.swt.internal.graphics.Graphics;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Manages colors that will be applied to forms and form widgets. The colors are
+ * chosen to make the widgets look correct in the editor area. If a different
+ * set of colors is needed, subclass this class and override 'initialize' and/or
+ * 'initializeColors'.
+ * 
+ * @since 1.0
+ */
+public class FormColors {
+       /**
+        * Key for the form title foreground color.
+        * 
+        * @deprecated use <code>IFormColors.TITLE</code>.
+        */
+       public static final String TITLE = IFormColors.TITLE;
+
+       /**
+        * Key for the tree/table border color.
+        * 
+        * @deprecated use <code>IFormColors.BORDER</code>
+        */
+       public static final String BORDER = IFormColors.BORDER;
+
+       /**
+        * Key for the section separator color.
+        * 
+        * @deprecated use <code>IFormColors.SEPARATOR</code>.
+        */
+       public static final String SEPARATOR = IFormColors.SEPARATOR;
+
+       /**
+        * Key for the section title bar background.
+        * 
+        * @deprecated use <code>IFormColors.TB_BG
+        */
+       public static final String TB_BG = IFormColors.TB_BG;
+
+       /**
+        * Key for the section title bar foreground.
+        * 
+        * @deprecated use <code>IFormColors.TB_FG</code>
+        */
+       public static final String TB_FG = IFormColors.TB_FG;
+
+       /**
+        * Key for the section title bar gradient.
+        * 
+        * @deprecated use <code>IFormColors.TB_GBG</code>
+        */
+       public static final String TB_GBG = IFormColors.TB_GBG;
+
+       /**
+        * Key for the section title bar border.
+        * 
+        * @deprecated use <code>IFormColors.TB_BORDER</code>.
+        */
+       public static final String TB_BORDER = IFormColors.TB_BORDER;
+
+       /**
+        * Key for the section toggle color. Since 3.1, this color is used for all
+        * section styles.
+        * 
+        * @deprecated use <code>IFormColors.TB_TOGGLE</code>.
+        */
+       public static final String TB_TOGGLE = IFormColors.TB_TOGGLE;
+
+       /**
+        * Key for the section toggle hover color.
+        * 
+        * @deprecated use <code>IFormColors.TB_TOGGLE_HOVER</code>.
+        */
+       public static final String TB_TOGGLE_HOVER = IFormColors.TB_TOGGLE_HOVER;
+
+       protected Map colorRegistry = new HashMap(10);
+
+       private LocalResourceManager resources;
+
+       protected Color background;
+
+       protected Color foreground;
+
+       private boolean shared;
+
+       protected Display display;
+
+       protected Color border;
+
+       /**
+        * Creates form colors using the provided display.
+        * 
+        * @param display
+        *            the display to use
+        */
+       public FormColors(Display display) {
+               this.display = display;
+               initialize();
+       }
+
+       /**
+        * Returns the display used to create colors.
+        * 
+        * @return the display
+        */
+       public Display getDisplay() {
+               return display;
+       }
+
+       /**
+        * Initializes the colors. Subclasses can override this method to change the
+        * way colors are created. Alternatively, only the color table can be
+        * modified by overriding <code>initializeColorTable()</code>.
+        * 
+        * @see #initializeColorTable
+        */
+       protected void initialize() {
+               background = display.getSystemColor(SWT.COLOR_LIST_BACKGROUND);
+               foreground = display.getSystemColor(SWT.COLOR_LIST_FOREGROUND);
+               initializeColorTable();
+               updateBorderColor();
+       }
+
+       /**
+        * Allocates colors for the following keys: BORDER, SEPARATOR and
+        * TITLE. Subclasses can override to allocate these colors differently.
+        */
+       protected void initializeColorTable() {
+               createTitleColor();
+               createColor(IFormColors.SEPARATOR, getColor(IFormColors.TITLE).getRGB());
+               RGB black = getSystemColor(SWT.COLOR_BLACK);
+               RGB borderRGB = getSystemColor(SWT.COLOR_TITLE_INACTIVE_BACKGROUND_GRADIENT);
+               createColor(IFormColors.BORDER, blend(borderRGB, black, 80));
+       }
+
+       /**
+        * Allocates colors for the section tool bar (all the keys that start with
+        * TB). Since these colors are only needed when TITLE_BAR style is used with
+        * the Section widget, they are not needed all the time and are allocated on
+        * demand. Consequently, this method will do nothing if the colors have been
+        * already initialized. Call this method prior to using colors with the TB
+        * keys to ensure they are available.
+        */
+       public void initializeSectionToolBarColors() {
+               if (colorRegistry.containsKey(IFormColors.TB_BG))
+                       return;
+               createTitleBarGradientColors();
+               createTitleBarOutlineColors();
+               createTwistieColors();
+       }
+
+       /**
+        * Allocates additional colors for the form header, namely background
+        * gradients, bottom separator keylines and DND highlights. Since these
+        * colors are only needed for clients that want to use these particular
+        * style of header rendering, they are not needed all the time and are
+        * allocated on demand. Consequently, this method will do nothing if the
+        * colors have been already initialized. Call this method prior to using
+        * color keys with the H_ prefix to ensure they are available.
+        */
+       protected void initializeFormHeaderColors() {
+               if (colorRegistry.containsKey(IFormColors.H_BOTTOM_KEYLINE2))
+                       return;
+               createFormHeaderColors();
+       }
+
+       /**
+        * Returns the RGB value of the system color represented by the code
+        * argument, as defined in <code>SWT</code> class.
+        * 
+        * @param code
+        *            the system color constant as defined in <code>SWT</code>
+        *            class.
+        * @return the RGB value of the system color
+        */
+       public RGB getSystemColor(int code) {
+               return getDisplay().getSystemColor(code).getRGB();
+       }
+
+       /**
+        * Creates the color for the specified key using the provided RGB object.
+        * The color object will be returned and also put into the registry. When
+        * the class is disposed, the color will be disposed with it.
+        * 
+        * @param key
+        *            the unique color key
+        * @param rgb
+        *            the RGB object
+        * @return the allocated color object
+        */
+       public Color createColor(String key, RGB rgb) {
+               // RAP [rh] changes due to missing Color constructor
+//             Color c = getResourceManager().createColor(rgb);
+//             Color prevC = (Color) colorRegistry.get(key);
+//             if (prevC != null && !prevC.isDisposed())
+//                     getResourceManager().destroyColor(prevC.getRGB());
+//             Color c = Graphics.getColor(rgb);
+               Color c = new Color(display, rgb);
+               colorRegistry.put(key, c);        
+               return c;
+       }
+
+       /**
+        * Creates a color that can be used for areas of the form that is inactive.
+        * These areas can contain images, links, controls and other content but are
+        * considered auxilliary to the main content area.
+        * 
+        * <p>
+        * The color should not be disposed because it is managed by this class.
+        * 
+        * @return the inactive form color
+        */
+       public Color getInactiveBackground() {
+               String key = "__ncbg__"; //$NON-NLS-1$
+               Color color = getColor(key);
+               if (color == null) {
+                       RGB sel = getSystemColor(SWT.COLOR_LIST_SELECTION);
+                       // a blend of 95% white and 5% list selection system color
+                       RGB ncbg = blend(sel, getSystemColor(SWT.COLOR_WHITE), 5);
+                       color = createColor(key, ncbg);
+               }
+               return color;
+       }
+
+       /**
+        * Creates the color for the specified key using the provided RGB values.
+        * The color object will be returned and also put into the registry. If
+        * there is already another color object under the same key in the registry,
+        * the existing object will be disposed. When the class is disposed, the
+        * color will be disposed with it.
+        * 
+        * @param key
+        *            the unique color key
+        * @param r
+        *            red value
+        * @param g
+        *            green value
+        * @param b
+        *            blue value
+        * @return the allocated color object
+        */
+       public Color createColor(String key, int r, int g, int b) {
+               return createColor(key, new RGB(r,g,b));
+       }
+
+       /**
+        * Computes the border color relative to the background. Allocated border
+        * color is designed to work well with white. Otherwise, stanard widget
+        * background color will be used.
+        */
+       protected void updateBorderColor() {
+               if (isWhiteBackground())
+                       border = getColor(IFormColors.BORDER);
+               else {
+                       border = display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+                       Color bg = getImpliedBackground();
+                       if (border.getRed() == bg.getRed()
+                                       && border.getGreen() == bg.getGreen()
+                                       && border.getBlue() == bg.getBlue())
+                               border = display.getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW);
+               }
+       }
+
+       /**
+        * Sets the background color. All the toolkits that use this class will
+        * share the same background.
+        * 
+        * @param bg
+        *            background color
+        */
+       public void setBackground(Color bg) {
+               this.background = bg;
+               updateBorderColor();
+               updateFormHeaderColors();
+       }
+
+       /**
+        * Sets the foreground color. All the toolkits that use this class will
+        * share the same foreground.
+        * 
+        * @param fg
+        *            foreground color
+        */
+       public void setForeground(Color fg) {
+               this.foreground = fg;
+       }
+
+       /**
+        * Returns the current background color.
+        * 
+        * @return the background color
+        */
+       public Color getBackground() {
+               return background;
+       }
+
+       /**
+        * Returns the current foreground color.
+        * 
+        * @return the foreground color
+        */
+       public Color getForeground() {
+               return foreground;
+       }
+
+       /**
+        * Returns the computed border color. Border color depends on the background
+        * and is recomputed whenever the background changes.
+        * 
+        * @return the current border color
+        */
+       public Color getBorderColor() {
+               return border;
+       }
+
+       /**
+        * Tests if the background is white. White background has RGB value
+        * 255,255,255.
+        * 
+        * @return <samp>true</samp> if background is white, <samp>false</samp>
+        *         otherwise.
+        */
+       public boolean isWhiteBackground() {
+               Color bg = getImpliedBackground();
+               return bg.getRed() == 255 && bg.getGreen() == 255
+                               && bg.getBlue() == 255;
+       }
+
+       /**
+        * Returns the color object for the provided key or <samp>null </samp> if
+        * not in the registry.
+        * 
+        * @param key
+        *            the color key
+        * @return color object if found, or <samp>null </samp> if not.
+        */
+       public Color getColor(String key) {
+               if (key.startsWith(IFormColors.TB_PREFIX))
+                       initializeSectionToolBarColors();
+               else if (key.startsWith(IFormColors.H_PREFIX))
+                       initializeFormHeaderColors();
+               return (Color) colorRegistry.get(key);
+       }
+
+       /**
+        * Disposes all the colors in the registry.
+        */
+       public void dispose() {
+               if (resources != null)
+                       resources.dispose();
+               resources = null;
+               colorRegistry = null;
+       }
+
+       /**
+        * Marks the colors shared. This prevents toolkits that share this object
+        * from disposing it.
+        */
+       public void markShared() {
+               this.shared = true;
+       }
+
+       /**
+        * Tests if the colors are shared.
+        * 
+        * @return <code>true</code> if shared, <code>false</code> otherwise.
+        */
+       public boolean isShared() {
+               return shared;
+       }
+
+       /**
+        * Blends c1 and c2 based in the provided ratio.
+        * 
+        * @param c1
+        *            first color
+        * @param c2
+        *            second color
+        * @param ratio
+        *            percentage of the first color in the blend (0-100)
+        * @return the RGB value of the blended color
+        */
+       public static RGB blend(RGB c1, RGB c2, int ratio) {
+               int r = blend(c1.red, c2.red, ratio);
+               int g = blend(c1.green, c2.green, ratio);
+               int b = blend(c1.blue, c2.blue, ratio);
+               return new RGB(r, g, b);
+       }
+
+       /**
+        * Tests the source RGB for range.
+        * 
+        * @param rgb
+        *            the tested RGB
+        * @param from
+        *            range start (excluding the value itself)
+        * @param to
+        *            range end (excluding the value itself)
+        * @return <code>true</code> if at least one of the primary colors in the
+        *         source RGB are within the provided range, <code>false</code>
+        *         otherwise.
+        */
+       public static boolean testAnyPrimaryColor(RGB rgb, int from, int to) {
+               if (testPrimaryColor(rgb.red, from, to))
+                       return true;
+               if (testPrimaryColor(rgb.green, from, to))
+                       return true;
+               if (testPrimaryColor(rgb.blue, from, to))
+                       return true;
+               return false;
+       }
+
+       /**
+        * Tests the source RGB for range.
+        * 
+        * @param rgb
+        *            the tested RGB
+        * @param from
+        *            range start (excluding the value itself)
+        * @param to
+        *            tange end (excluding the value itself)
+        * @return <code>true</code> if at least two of the primary colors in the
+        *         source RGB are within the provided range, <code>false</code>
+        *         otherwise.
+        */
+       public static boolean testTwoPrimaryColors(RGB rgb, int from, int to) {
+               int total = 0;
+               if (testPrimaryColor(rgb.red, from, to))
+                       total++;
+               if (testPrimaryColor(rgb.green, from, to))
+                       total++;
+               if (testPrimaryColor(rgb.blue, from, to))
+                       total++;
+               return total >= 2;
+       }
+
+       /**
+        * Blends two primary color components based on the provided ratio.
+        * 
+        * @param v1
+        *            first component
+        * @param v2
+        *            second component
+        * @param ratio
+        *            percentage of the first component in the blend
+        * @return
+        */
+       private static int blend(int v1, int v2, int ratio) {
+               int b = (ratio * v1 + (100 - ratio) * v2) / 100;
+               return Math.min(255, b);
+       }
+
+       private Color getImpliedBackground() {
+               if (getBackground() != null)
+                       return getBackground();
+               return getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+       }
+
+       private static boolean testPrimaryColor(int value, int from, int to) {
+               return value > from && value < to;
+       }
+
+       private void createTitleColor() {
+               /*
+                * RGB rgb = getSystemColor(SWT.COLOR_LIST_SELECTION); // test too light
+                * if (testTwoPrimaryColors(rgb, 120, 151)) rgb = blend(rgb, BLACK, 80);
+                * else if (testTwoPrimaryColors(rgb, 150, 256)) rgb = blend(rgb, BLACK,
+                * 50); createColor(TITLE, rgb);
+                */
+               RGB bg = getImpliedBackground().getRGB();
+               RGB listSelection = getSystemColor(SWT.COLOR_LIST_SELECTION);
+               RGB listForeground = getSystemColor(SWT.COLOR_LIST_FOREGROUND);
+               RGB rgb = listSelection;
+
+               // Group 1
+               // Rule: If at least 2 of the LIST_SELECTION RGB values are equal to or
+               // between 0 and 120, then use 100% LIST_SELECTION as it is (no
+               // additions)
+               // Examples: XP Default, Win Classic Standard, Win High Con White, Win
+               // Classic Marine
+               if (testTwoPrimaryColors(listSelection, -1, 121))
+                       rgb = listSelection;
+               // Group 2
+               // When LIST_BACKGROUND = white (255, 255, 255) or not black, text
+               // colour = LIST_SELECTION @ 100% Opacity + 50% LIST_FOREGROUND over
+               // LIST_BACKGROUND
+               // Rule: If at least 2 of the LIST_SELECTION RGB values are equal to or
+               // between 121 and 255, then add 50% LIST_FOREGROUND to LIST_SELECTION
+               // foreground colour
+               // Examples: Win Vista, XP Silver, XP Olive , Win Classic Plum, OSX
+               // Aqua, OSX Graphite, Linux GTK
+               else if (testTwoPrimaryColors(listSelection, 120, 256)
+                               || (bg.red == 0 && bg.green == 0 && bg.blue == 0))
+                       rgb = blend(listSelection, listForeground, 50);
+               // Group 3
+               // When LIST_BACKGROUND = black (0, 0, 0), text colour = LIST_SELECTION
+               // @ 100% Opacity + 50% LIST_FOREGROUND over LIST_BACKGROUND
+               // Rule: If LIST_BACKGROUND = 0, 0, 0, then add 50% LIST_FOREGROUND to
+               // LIST_SELECTION foreground colour
+               // Examples: Win High Con Black, Win High Con #1, Win High Con #2
+               // (covered in the second part of the OR clause above)
+               createColor(IFormColors.TITLE, rgb);
+       }
+
+       private void createTwistieColors() {
+               RGB rgb = getColor(IFormColors.TITLE).getRGB();
+               RGB white = getSystemColor(SWT.COLOR_WHITE);
+               createColor(TB_TOGGLE, rgb);
+               rgb = blend(rgb, white, 60);
+               createColor(TB_TOGGLE_HOVER, rgb);
+       }
+
+       private void createTitleBarGradientColors() {
+               RGB tbBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+               RGB bg = getImpliedBackground().getRGB();
+
+               // Group 1
+               // Rule: If at least 2 of the RGB values are equal to or between 180 and
+               // 255, then apply specified opacity for Group 1
+               // Examples: Vista, XP Silver, Wn High Con #2
+               // Gradient Bottom = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               if (testTwoPrimaryColors(tbBg, 179, 256))
+                       tbBg = blend(tbBg, bg, 30);
+
+               // Group 2
+               // Rule: If at least 2 of the RGB values are equal to or between 121 and
+               // 179, then apply specified opacity for Group 2
+               // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+               // Gradient Bottom = TITLE_BACKGROUND @ 20% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               else if (testTwoPrimaryColors(tbBg, 120, 180))
+                       tbBg = blend(tbBg, bg, 20);
+
+               // Group 3
+               // Rule: Everything else
+               // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+               // Aqua, Wn High Con White, Wn High Con #1
+               // Gradient Bottom = TITLE_BACKGROUND @ 10% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               else {
+                       tbBg = blend(tbBg, bg, 10);
+               }
+
+               createColor(IFormColors.TB_BG, tbBg);
+               
+               // for backward compatibility
+               createColor(TB_GBG, tbBg);
+       }
+
+       private void createTitleBarOutlineColors() {
+               // title bar outline - border color
+               RGB tbBorder = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+               RGB bg = getImpliedBackground().getRGB();
+               // Group 1
+               // Rule: If at least 2 of the RGB values are equal to or between 180 and
+               // 255, then apply specified opacity for Group 1
+               // Examples: Vista, XP Silver, Wn High Con #2
+               // Keyline = TITLE_BACKGROUND @ 70% Opacity over LIST_BACKGROUND
+               if (testTwoPrimaryColors(tbBorder, 179, 256))
+                       tbBorder = blend(tbBorder, bg, 70);
+
+               // Group 2
+               // Rule: If at least 2 of the RGB values are equal to or between 121 and
+               // 179, then apply specified opacity for Group 2
+               // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+
+               // Keyline = TITLE_BACKGROUND @ 50% Opacity over LIST_BACKGROUND
+               else if (testTwoPrimaryColors(tbBorder, 120, 180))
+                       tbBorder = blend(tbBorder, bg, 50);
+
+               // Group 3
+               // Rule: Everything else
+               // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+               // Aqua, Wn High Con White, Wn High Con #1
+
+               // Keyline = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+               else {
+                       tbBorder = blend(tbBorder, bg, 30);
+               }
+               createColor(FormColors.TB_BORDER, tbBorder);
+       }
+
+       private void updateFormHeaderColors() {
+               if (colorRegistry.containsKey(IFormColors.H_GRADIENT_END)) {
+                       disposeIfFound(IFormColors.H_GRADIENT_END);
+                       disposeIfFound(IFormColors.H_GRADIENT_START);
+                       disposeIfFound(IFormColors.H_BOTTOM_KEYLINE1);
+                       disposeIfFound(IFormColors.H_BOTTOM_KEYLINE2);
+                       disposeIfFound(IFormColors.H_HOVER_LIGHT);
+                       disposeIfFound(IFormColors.H_HOVER_FULL);
+                       initializeFormHeaderColors();
+               }
+       }
+
+       private void disposeIfFound(String key) {
+               Color color = getColor(key);
+               if (color != null) {
+                       colorRegistry.remove(key);
+               // RAP [rh] changes due to missing Color#dispose()                      
+//                     color.dispose();
+               }
+       }
+
+       private void createFormHeaderColors() {
+               createFormHeaderGradientColors();
+               createFormHeaderKeylineColors();
+               createFormHeaderDNDColors();
+       }
+
+       private void createFormHeaderGradientColors() {
+               RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+               Color bgColor = getImpliedBackground();
+               RGB bg = bgColor.getRGB();
+               RGB bottom, top;
+               // Group 1
+               // Rule: If at least 2 of the RGB values are equal to or between 180 and
+               // 255, then apply specified opacity for Group 1
+               // Examples: Vista, XP Silver, Wn High Con #2
+               // Gradient Bottom = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               if (testTwoPrimaryColors(titleBg, 179, 256)) {
+                       bottom = blend(titleBg, bg, 30);
+                       top = bg;
+               }
+
+               // Group 2
+               // Rule: If at least 2 of the RGB values are equal to or between 121 and
+               // 179, then apply specified opacity for Group 2
+               // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+               // Gradient Bottom = TITLE_BACKGROUND @ 20% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               else if (testTwoPrimaryColors(titleBg, 120, 180)) {
+                       bottom = blend(titleBg, bg, 20);
+                       top = bg;
+               }
+
+               // Group 3
+               // Rule: If at least 2 of the RGB values are equal to or between 0 and
+               // 120, then apply specified opacity for Group 3
+               // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+               // Aqua, Wn High Con White, Wn High Con #1
+               // Gradient Bottom = TITLE_BACKGROUND @ 10% Opacity over LIST_BACKGROUND
+               // Gradient Top = TITLE BACKGROUND @ 0% Opacity over LIST_BACKGROUND
+               else {
+                       bottom = blend(titleBg, bg, 10);
+                       top = bg;
+               }
+               createColor(IFormColors.H_GRADIENT_END, top);
+               createColor(IFormColors.H_GRADIENT_START, bottom);
+       }
+
+       private void createFormHeaderKeylineColors() {
+               RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
+               Color bgColor = getImpliedBackground();
+               RGB bg = bgColor.getRGB();
+               RGB keyline2;
+               // H_BOTTOM_KEYLINE1
+               createColor(IFormColors.H_BOTTOM_KEYLINE1, new RGB(255, 255, 255));
+
+               // H_BOTTOM_KEYLINE2
+               // Group 1
+               // Rule: If at least 2 of the RGB values are equal to or between 180 and
+               // 255, then apply specified opacity for Group 1
+               // Examples: Vista, XP Silver, Wn High Con #2
+               // Keyline = TITLE_BACKGROUND @ 70% Opacity over LIST_BACKGROUND
+               if (testTwoPrimaryColors(titleBg, 179, 256))
+                       keyline2 = blend(titleBg, bg, 70);
+
+               // Group 2
+               // Rule: If at least 2 of the RGB values are equal to or between 121 and
+               // 179, then apply specified opacity for Group 2
+               // Examples: XP Olive, OSX Graphite, Linux GTK, Wn High Con Black
+               // Keyline = TITLE_BACKGROUND @ 50% Opacity over LIST_BACKGROUND
+               else if (testTwoPrimaryColors(titleBg, 120, 180))
+                       keyline2 = blend(titleBg, bg, 50);
+
+               // Group 3
+               // Rule: If at least 2 of the RGB values are equal to or between 0 and
+               // 120, then apply specified opacity for Group 3
+               // Examples: XP Default, Wn Classic Standard, Wn Marine, Wn Plum, OSX
+               // Aqua, Wn High Con White, Wn High Con #1
+
+               // Keyline = TITLE_BACKGROUND @ 30% Opacity over LIST_BACKGROUND
+               else
+                       keyline2 = blend(titleBg, bg, 30);
+               // H_BOTTOM_KEYLINE2
+               createColor(IFormColors.H_BOTTOM_KEYLINE2, keyline2);
+       }
+
+       private void createFormHeaderDNDColors() {
+               RGB titleBg = getSystemColor(SWT.COLOR_TITLE_BACKGROUND_GRADIENT);
+               Color bgColor = getImpliedBackground();
+               RGB bg = bgColor.getRGB();
+               RGB light, full;
+               // ALL Themes
+               //
+               // Light Highlight
+               // When *near* the 'hot' area
+               // Rule: If near the title in the 'hot' area, show background highlight
+               // TITLE_BACKGROUND_GRADIENT @ 40%
+               light = blend(titleBg, bg, 40);
+               // Full Highlight
+               // When *on* the title area (regions 1 and 2)
+               // Rule: If near the title in the 'hot' area, show background highlight
+               // TITLE_BACKGROUND_GRADIENT @ 60%
+               full = blend(titleBg, bg, 60);
+               // H_DND_LIGHT
+               // H_DND_FULL
+               createColor(IFormColors.H_HOVER_LIGHT, light);
+               createColor(IFormColors.H_HOVER_FULL, full);
+       }
+       
+       private LocalResourceManager getResourceManager() {
+               if (resources == null)
+                       resources = new LocalResourceManager(JFaceResources.getResources());
+               return resources;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java
new file mode 100644 (file)
index 0000000..9e931ba
--- /dev/null
@@ -0,0 +1,122 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import java.util.HashMap;
+
+import org.eclipse.jface.resource.DeviceResourceException;
+import org.eclipse.jface.resource.FontDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+//import org.eclipse.swt.internal.graphics.Graphics;
+import org.eclipse.swt.widgets.Display;
+
+public class FormFonts {
+       private static FormFonts instance;
+
+       public static FormFonts getInstance() {
+               if (instance == null)
+                       instance = new FormFonts();
+               return instance;
+       }
+
+       private LocalResourceManager resources;
+       private HashMap descriptors;
+
+       private FormFonts() {
+       }
+
+       private class BoldFontDescriptor extends FontDescriptor {
+               private FontData[] fFontData;
+
+               BoldFontDescriptor(Font font) {
+                       // RAP [if] Changes due to different way of creating fonts
+                       // fFontData = font.getFontData();
+                       // for (int i = 0; i < fFontData.length; i++) {
+                       // fFontData[i].setStyle(fFontData[i].getStyle() | SWT.BOLD);
+                       // }
+                       FontData fontData = font.getFontData()[0];
+                       // Font boldFont = Graphics.getFont( fontData.getName(),
+                       // fontData.getHeight(),
+                       // fontData.getStyle() | SWT.BOLD );
+                       Font boldFont = new Font(Display.getCurrent(), fontData.getName(), fontData.getHeight(),
+                                       fontData.getStyle() | SWT.BOLD);
+                       fFontData = boldFont.getFontData();
+               }
+
+               public boolean equals(Object obj) {
+                       if (obj instanceof BoldFontDescriptor) {
+                               BoldFontDescriptor desc = (BoldFontDescriptor) obj;
+                               if (desc.fFontData.length != fFontData.length)
+                                       return false;
+                               for (int i = 0; i < fFontData.length; i++)
+                                       if (!fFontData[i].equals(desc.fFontData[i]))
+                                               return false;
+                               return true;
+                       }
+                       return false;
+               }
+
+               public int hashCode() {
+                       int hash = 0;
+                       for (int i = 0; i < fFontData.length; i++)
+                               hash = hash * 7 + fFontData[i].hashCode();
+                       return hash;
+               }
+
+               public Font createFont(Device device) throws DeviceResourceException {
+                       // RAP [if] Changes due to different way of creating fonts
+                       return new Font(device, fFontData[0]);
+                       // return Graphics.getFont( fFontData[ 0 ] );
+               }
+
+               public void destroyFont(Font previouslyCreatedFont) {
+                       // RAP [if] unnecessary
+                       // previouslyCreatedFont.dispose();
+               }
+       }
+
+       public Font getBoldFont(Display display, Font font) {
+               checkHashMaps();
+               BoldFontDescriptor desc = new BoldFontDescriptor(font);
+               Font result = getResourceManager().createFont(desc);
+               descriptors.put(result, desc);
+               return result;
+       }
+
+       public boolean markFinished(Font boldFont) {
+               checkHashMaps();
+               BoldFontDescriptor desc = (BoldFontDescriptor) descriptors.get(boldFont);
+               if (desc != null) {
+                       getResourceManager().destroyFont(desc);
+                       if (getResourceManager().find(desc) == null) {
+                               descriptors.remove(boldFont);
+                               validateHashMaps();
+                       }
+                       return true;
+
+               }
+               // if the image was not found, dispose of it for the caller
+               // RAP [if] unnecessary
+               // boldFont.dispose();
+               return false;
+       }
+
+       private LocalResourceManager getResourceManager() {
+               if (resources == null)
+                       resources = new LocalResourceManager(JFaceResources.getResources());
+               return resources;
+       }
+
+       private void checkHashMaps() {
+               if (descriptors == null)
+                       descriptors = new HashMap();
+       }
+
+       private void validateHashMaps() {
+               if (descriptors.size() == 0)
+                       descriptors = null;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java
new file mode 100644 (file)
index 0000000..9927104
--- /dev/null
@@ -0,0 +1,913 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+//import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.FocusAdapter;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+// RAP [rh] Paint events missing
+//import org.eclipse.swt.events.PaintEvent;
+//import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+//RAP [rh] GC missing
+//import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Point;
+//import org.eclipse.swt.graphics.RGB;
+//import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+//import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Label;
+//import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.Widget;
+//import org.eclipse.ui.forms.FormColors;
+//import org.eclipse.ui.forms.HyperlinkGroup;
+//import org.eclipse.ui.forms.IFormColors;
+//import org.eclipse.ui.internal.forms.widgets.FormFonts;
+//import org.eclipse.ui.internal.forms.widgets.FormUtil;
+
+/**
+ * The toolkit is responsible for creating SWT controls adapted to work in
+ * Eclipse forms. In addition to changing their presentation properties (fonts,
+ * colors etc.), various listeners are attached to make them behave correctly in
+ * the form context.
+ * <p>
+ * In addition to being the control factory, the toolkit is also responsible for
+ * painting flat borders for select controls, managing hyperlink groups and
+ * control colors.
+ * <p>
+ * The toolkit creates some of the most common controls used to populate Eclipse
+ * forms. Controls that must be created using their constructors,
+ * <code>adapt()</code> method is available to change its properties in the
+ * same way as with the supported toolkit controls.
+ * <p>
+ * Typically, one toolkit object is created per workbench part (for example, an
+ * editor or a form wizard). The toolkit is disposed when the part is disposed.
+ * To conserve resources, it is possible to create one color object for the
+ * entire plug-in and share it between several toolkits. The plug-in is
+ * responsible for disposing the colors (disposing the toolkit that uses shared
+ * color object will not dispose the colors).
+ * <p>
+ * FormToolkit is normally instantiated, but can also be subclassed if some of
+ * the methods needs to be modified. In those cases, <code>super</code> must
+ * be called to preserve normal behaviour.
+ *
+ * @since 1.0
+ */
+public class FormToolkit {
+       public static final String KEY_DRAW_BORDER = "FormWidgetFactory.drawBorder"; //$NON-NLS-1$
+
+       public static final String TREE_BORDER = "treeBorder"; //$NON-NLS-1$
+
+       public static final String TEXT_BORDER = "textBorder"; //$NON-NLS-1$
+
+       private int borderStyle = SWT.NULL;
+
+       private FormColors colors;
+
+       private int orientation = Window.getDefaultOrientation();
+
+       // private KeyListener deleteListener;
+       // RAP [rh] Paint events missing
+//     private BorderPainter borderPainter;
+
+       private BoldFontHolder boldFontHolder;
+
+//     private HyperlinkGroup hyperlinkGroup;
+       
+       private boolean isDisposed = false;
+
+       /* default */
+       VisibilityHandler visibilityHandler;
+
+       /* default */
+       KeyboardHandler keyboardHandler;
+
+       // RAP [rh] Paint events missing
+//     private class BorderPainter implements PaintListener {
+//             public void paintControl(PaintEvent event) {
+//                     Composite composite = (Composite) event.widget;
+//                     Control[] children = composite.getChildren();
+//                     for (int i = 0; i < children.length; i++) {
+//                             Control c = children[i];
+//                             boolean inactiveBorder = false;
+//                             boolean textBorder = false;
+//                             if (!c.isVisible())
+//                                     continue;
+//                             /*
+//                              * if (c.getEnabled() == false && !(c instanceof CCombo))
+//                              * continue;
+//                              */
+//                             if (c instanceof Hyperlink)
+//                                     continue;
+//                             Object flag = c.getData(KEY_DRAW_BORDER);
+//                             if (flag != null) {
+//                                     if (flag.equals(Boolean.FALSE))
+//                                             continue;
+//                                     if (flag.equals(TREE_BORDER))
+//                                             inactiveBorder = true;
+//                                     else if (flag.equals(TEXT_BORDER))
+//                                             textBorder = true;
+//                             }
+//                             if (getBorderStyle() == SWT.BORDER) {
+//                                     if (!inactiveBorder && !textBorder) {
+//                                             continue;
+//                                     }
+//                                     if (c instanceof Text || c instanceof Table
+//                                                     || c instanceof Tree)
+//                                             continue;
+//                             }
+//                             if (!inactiveBorder
+//                                             && (c instanceof Text || c instanceof CCombo || textBorder)) {
+//                                     Rectangle b = c.getBounds();
+//                                     GC gc = event.gc;
+//                                     gc.setForeground(c.getBackground());
+//                                     gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1,
+//                                                     b.height + 1);
+//                                     // gc.setForeground(getBorderStyle() == SWT.BORDER ? colors
+//                                     // .getBorderColor() : colors.getForeground());
+//                                     gc.setForeground(colors.getBorderColor());
+//                                     if (c instanceof CCombo)
+//                                             gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1,
+//                                                             b.height + 1);
+//                                     else
+//                                             gc.drawRectangle(b.x - 1, b.y - 2, b.width + 1,
+//                                                             b.height + 3);
+//                             } else if (inactiveBorder || c instanceof Table
+//                                             || c instanceof Tree) {
+//                                     Rectangle b = c.getBounds();
+//                                     GC gc = event.gc;
+//                                     gc.setForeground(colors.getBorderColor());
+//                                     gc.drawRectangle(b.x - 1, b.y - 1, b.width + 1,
+//                                                     b.height + 1);
+//                             }
+//                     }
+//             }
+//     }
+
+       private static class VisibilityHandler extends FocusAdapter {
+               public void focusGained(FocusEvent e) {
+                       Widget w = e.widget;
+                       if (w instanceof Control) {
+                               FormUtil.ensureVisible((Control) w);
+                       }
+               }
+       }
+
+       private static class KeyboardHandler extends KeyAdapter {
+               public void keyPressed(KeyEvent e) {
+                       Widget w = e.widget;
+                       if (w instanceof Control) {
+                               if (e.doit)
+                                       FormUtil.processKey(e.keyCode, (Control) w);
+                       }
+               }
+       }
+
+       private class BoldFontHolder {
+               private Font normalFont;
+
+               private Font boldFont;
+
+               public BoldFontHolder() {
+               }
+
+               public Font getBoldFont(Font font) {
+                       createBoldFont(font);
+                       return boldFont;
+               }
+
+               private void createBoldFont(Font font) {
+                       if (normalFont == null || !normalFont.equals(font)) {
+                               normalFont = font;
+                               dispose();
+                       }
+                       if (boldFont == null) {
+                               boldFont = FormFonts.getInstance().getBoldFont(colors.getDisplay(),
+                                               normalFont);
+                       }
+               }
+
+               public void dispose() {
+                       if (boldFont != null) {
+                               FormFonts.getInstance().markFinished(boldFont);
+                               boldFont = null;
+                       }
+               }
+       }
+
+       /**
+        * Creates a toolkit that is self-sufficient (will manage its own colors).
+        * <p>
+        * Clients that call this method must call {@link #dispose()} when they
+        * are finished using the toolkit.
+        *
+        */
+       public FormToolkit(Display display) {
+               this(new FormColors(display));
+       }
+
+       /**
+        * Creates a toolkit that will use the provided (shared) colors. The toolkit
+        * will dispose the colors if and only if they are <b>not</b> marked as
+        * shared via the <code>markShared()</code> method.
+        * <p>
+        * Clients that call this method must call {@link #dispose()} when they
+        * are finished using the toolkit.
+        *
+        * @param colors
+        *            the shared colors
+        */
+       public FormToolkit(FormColors colors) {
+               this.colors = colors;
+               initialize();
+       }
+
+       /**
+        * Creates a button as a part of the form.
+        *
+        * @param parent
+        *            the button parent
+        * @param text
+        *            an optional text for the button (can be <code>null</code>)
+        * @param style
+        *            the button style (for example, <code>SWT.PUSH</code>)
+        * @return the button widget
+        */
+       public Button createButton(Composite parent, String text, int style) {
+               Button button = new Button(parent, style | SWT.FLAT | orientation);
+               if (text != null)
+                       button.setText(text);
+               adapt(button, true, true);
+               return button;
+       }
+
+       /**
+        * Creates the composite as a part of the form.
+        *
+        * @param parent
+        *            the composite parent
+        * @return the composite widget
+        */
+       public Composite createComposite(Composite parent) {
+               return createComposite(parent, SWT.NULL);
+       }
+
+       /**
+        * Creates the composite as part of the form using the provided style.
+        *
+        * @param parent
+        *            the composite parent
+        * @param style
+        *            the composite style
+        * @return the composite widget
+        */
+       public Composite createComposite(Composite parent, int style) {
+//             Composite composite = new LayoutComposite(parent, style | orientation);
+               Composite composite = new Composite(parent, style | orientation);
+               adapt(composite);
+               return composite;
+       }
+
+       /**
+        * Creats the composite that can server as a separator between various parts
+        * of a form. Separator height should be controlled by setting the height
+        * hint on the layout data for the composite.
+        *
+        * @param parent
+        *            the separator parent
+        * @return the separator widget
+        */
+// RAP [rh] createCompositeSeparator: currently no useful implementation possible, delete?
+       public Composite createCompositeSeparator(Composite parent) {
+               final Composite composite = new Composite(parent, orientation);
+// RAP [rh] GC and paint events missing
+//             composite.addListener(SWT.Paint, new Listener() {
+//                     public void handleEvent(Event e) {
+//                             if (composite.isDisposed())
+//                                     return;
+//                             Rectangle bounds = composite.getBounds();
+//                             GC gc = e.gc;
+//                             gc.setForeground(colors.getColor(IFormColors.SEPARATOR));
+//                             if (colors.getBackground() != null)
+//                                     gc.setBackground(colors.getBackground());
+//                             gc.fillGradientRectangle(0, 0, bounds.width, bounds.height,
+//                                             false);
+//                     }
+//             });
+//             if (parent instanceof Section)
+//                     ((Section) parent).setSeparatorControl(composite);
+               return composite;
+       }
+
+       /**
+        * Creates a label as a part of the form.
+        *
+        * @param parent
+        *            the label parent
+        * @param text
+        *            the label text
+        * @return the label widget
+        */
+       public Label createLabel(Composite parent, String text) {
+               return createLabel(parent, text, SWT.NONE);
+       }
+
+       /**
+        * Creates a label as a part of the form.
+        *
+        * @param parent
+        *            the label parent
+        * @param text
+        *            the label text
+        * @param style
+        *            the label style
+        * @return the label widget
+        */
+       public Label createLabel(Composite parent, String text, int style) {
+               Label label = new Label(parent, style | orientation);
+               if (text != null)
+                       label.setText(text);
+               adapt(label, false, false);
+               return label;
+       }
+
+       /**
+        * Creates a hyperlink as a part of the form. The hyperlink will be added to
+        * the hyperlink group that belongs to this toolkit.
+        *
+        * @param parent
+        *            the hyperlink parent
+        * @param text
+        *            the text of the hyperlink
+        * @param style
+        *            the hyperlink style
+        * @return the hyperlink widget
+        */
+//     public Hyperlink createHyperlink(Composite parent, String text, int style) {
+//             Hyperlink hyperlink = new Hyperlink(parent, style | orientation);
+//             if (text != null)
+//                     hyperlink.setText(text);
+//             hyperlink.addFocusListener(visibilityHandler);
+//             hyperlink.addKeyListener(keyboardHandler);
+//             hyperlinkGroup.add(hyperlink);
+//             return hyperlink;
+//     }
+
+       /**
+        * Creates an image hyperlink as a part of the form. The hyperlink will be
+        * added to the hyperlink group that belongs to this toolkit.
+        *
+        * @param parent
+        *            the hyperlink parent
+        * @param style
+        *            the hyperlink style
+        * @return the image hyperlink widget
+        */
+//     public ImageHyperlink createImageHyperlink(Composite parent, int style) {
+//             ImageHyperlink hyperlink = new ImageHyperlink(parent, style
+//                             | orientation);
+//             hyperlink.addFocusListener(visibilityHandler);
+//             hyperlink.addKeyListener(keyboardHandler);
+//             hyperlinkGroup.add(hyperlink);
+//             return hyperlink;
+//     }
+
+       /**
+        * Creates a rich text as a part of the form.
+        *
+        * @param parent
+        *            the rich text parent
+        * @param trackFocus
+        *            if <code>true</code>, the toolkit will monitor focus
+        *            transfers to ensure that the hyperlink in focus is visible in
+        *            the form.
+        * @return the rich text widget
+        * @since 1.2
+        */
+//     public FormText createFormText(Composite parent, boolean trackFocus) {
+//             FormText engine = new FormText(parent, SWT.WRAP | orientation);
+//             engine.marginWidth = 1;
+//             engine.marginHeight = 0;
+//             engine.setHyperlinkSettings(getHyperlinkGroup());
+//             adapt(engine, trackFocus, true);
+//             engine.setMenu(parent.getMenu());
+//             return engine;
+//     }
+
+       /**
+        * Adapts a control to be used in a form that is associated with this
+        * toolkit. This involves adjusting colors and optionally adding handlers to
+        * ensure focus tracking and keyboard management.
+        *
+        * @param control
+        *            a control to adapt
+        * @param trackFocus
+        *            if <code>true</code>, form will be scrolled horizontally
+        *            and/or vertically if needed to ensure that the control is
+        *            visible when it gains focus. Set it to <code>false</code> if
+        *            the control is not capable of gaining focus.
+        * @param trackKeyboard
+        *            if <code>true</code>, the control that is capable of
+        *            gaining focus will be tracked for certain keys that are
+        *            important to the underlying form (for example, PageUp,
+        *            PageDown, ScrollUp, ScrollDown etc.). Set it to
+        *            <code>false</code> if the control is not capable of gaining
+        *            focus or these particular key event are already used by the
+        *            control.
+        */
+       public void adapt(Control control, boolean trackFocus, boolean trackKeyboard) {
+               control.setBackground(colors.getBackground());
+               control.setForeground(colors.getForeground());
+//             if (control instanceof ExpandableComposite) {
+//                     ExpandableComposite ec = (ExpandableComposite) control;
+//                     if (ec.toggle != null) {
+//                             if (trackFocus)
+//                                     ec.toggle.addFocusListener(visibilityHandler);
+//                             if (trackKeyboard)
+//                                     ec.toggle.addKeyListener(keyboardHandler);
+//                     }
+//                     if (ec.textLabel != null) {
+//                             if (trackFocus)
+//                                     ec.textLabel.addFocusListener(visibilityHandler);
+//                             if (trackKeyboard)
+//                                     ec.textLabel.addKeyListener(keyboardHandler);
+//                     }
+//                     return;
+//             }
+               if (trackFocus)
+                       control.addFocusListener(visibilityHandler);
+               if (trackKeyboard)
+                       control.addKeyListener(keyboardHandler);
+       }
+
+       /**
+        * Adapts a composite to be used in a form associated with this toolkit.
+        *
+        * @param composite
+        *            the composite to adapt
+        */
+       public void adapt(Composite composite) {
+               composite.setBackground(colors.getBackground());
+               composite.addMouseListener(new MouseAdapter() {
+                       public void mouseDown(MouseEvent e) {
+                               ((Control) e.widget).setFocus();
+                       }
+               });
+               if (composite.getParent() != null)
+                       composite.setMenu(composite.getParent().getMenu());
+       }
+
+       /**
+        * A helper method that ensures the provided control is visible when
+        * ScrolledComposite is somewhere in the parent chain. If scroll bars are
+        * visible and the control is clipped, the client of the scrolled composite
+        * will be scrolled to reveal the control.
+        *
+        * @param c
+        *            the control to reveal
+        */
+       public static void ensureVisible(Control c) {
+               FormUtil.ensureVisible(c);
+       }
+
+       /**
+        * Creates a section as a part of the form.
+        *
+        * @param parent
+        *            the section parent
+        * @param sectionStyle
+        *            the section style
+        * @return the section widget
+        */
+//     public Section createSection(Composite parent, int sectionStyle) {
+//             Section section = new Section(parent, orientation, sectionStyle);
+//             section.setMenu(parent.getMenu());
+//             adapt(section, true, true);
+//             if (section.toggle != null) {
+//                     section.toggle.setHoverDecorationColor(colors
+//                                     .getColor(IFormColors.TB_TOGGLE_HOVER));
+//                     section.toggle.setDecorationColor(colors
+//                                     .getColor(IFormColors.TB_TOGGLE));
+//             }
+//             section.setFont(boldFontHolder.getBoldFont(parent.getFont()));
+//             if ((sectionStyle & Section.TITLE_BAR) != 0
+//                             || (sectionStyle & Section.SHORT_TITLE_BAR) != 0) {
+//                     colors.initializeSectionToolBarColors();
+//                     section.setTitleBarBackground(colors.getColor(IFormColors.TB_BG));
+//                     section.setTitleBarBorderColor(colors
+//                                     .getColor(IFormColors.TB_BORDER));
+//             }
+//             // call setTitleBarForeground regardless as it also sets the label color
+//             section.setTitleBarForeground(colors
+//                             .getColor(IFormColors.TB_TOGGLE));
+//             return section;
+//     }
+
+       /**
+        * Creates an expandable composite as a part of the form.
+        *
+        * @param parent
+        *            the expandable composite parent
+        * @param expansionStyle
+        *            the expandable composite style
+        * @return the expandable composite widget
+        */
+//     public ExpandableComposite createExpandableComposite(Composite parent,
+//                     int expansionStyle) {
+//             ExpandableComposite ec = new ExpandableComposite(parent, orientation,
+//                             expansionStyle);
+//             ec.setMenu(parent.getMenu());
+//             adapt(ec, true, true);
+//             ec.setFont(boldFontHolder.getBoldFont(ec.getFont()));
+//             return ec;
+//     }
+
+       /**
+        * Creates a separator label as a part of the form.
+        *
+        * @param parent
+        *            the separator parent
+        * @param style
+        *            the separator style
+        * @return the separator label
+        */
+       public Label createSeparator(Composite parent, int style) {
+               Label label = new Label(parent, SWT.SEPARATOR | style | orientation);
+               label.setBackground(colors.getBackground());
+               label.setForeground(colors.getBorderColor());
+               return label;
+       }
+
+       /**
+        * Creates a table as a part of the form.
+        *
+        * @param parent
+        *            the table parent
+        * @param style
+        *            the table style
+        * @return the table widget
+        */
+       public Table createTable(Composite parent, int style) {
+               Table table = new Table(parent, style | borderStyle | orientation);
+               adapt(table, false, false);
+               // hookDeleteListener(table);
+               return table;
+       }
+
+       /**
+        * Creates a text as a part of the form.
+        *
+        * @param parent
+        *            the text parent
+        * @param value
+        *            the text initial value
+        * @return the text widget
+        */
+       public Text createText(Composite parent, String value) {
+               return createText(parent, value, SWT.SINGLE);
+       }
+
+       /**
+        * Creates a text as a part of the form.
+        *
+        * @param parent
+        *            the text parent
+        * @param value
+        *            the text initial value
+        * @param style
+        *            the text style
+        * @return the text widget
+        */
+       public Text createText(Composite parent, String value, int style) {
+               Text text = new Text(parent, borderStyle | style | orientation);
+               if (value != null)
+                       text.setText(value);
+               text.setForeground(colors.getForeground());
+               text.setBackground(colors.getBackground());
+               text.addFocusListener(visibilityHandler);
+               return text;
+       }
+
+       /**
+        * Creates a tree widget as a part of the form.
+        *
+        * @param parent
+        *            the tree parent
+        * @param style
+        *            the tree style
+        * @return the tree widget
+        */
+       public Tree createTree(Composite parent, int style) {
+               Tree tree = new Tree(parent, borderStyle | style | orientation);
+               adapt(tree, false, false);
+               // hookDeleteListener(tree);
+               return tree;
+       }
+
+       /**
+        * Creates a scrolled form widget in the provided parent. If you do not
+        * require scrolling because there is already a scrolled composite up the
+        * parent chain, use 'createForm' instead.
+        *
+        * @param parent
+        *            the scrolled form parent
+        * @return the form that can scroll itself
+        * @see #createForm
+        */
+       public ScrolledComposite createScrolledForm(Composite parent) {
+               ScrolledComposite form = new ScrolledComposite(parent, SWT.V_SCROLL
+                               | SWT.H_SCROLL | orientation);
+               form.setExpandHorizontal(true);
+               form.setExpandVertical(true);
+               form.setBackground(colors.getBackground());
+               form.setForeground(colors.getColor(IFormColors.TITLE));
+               form.setFont(JFaceResources.getHeaderFont());
+               return form;
+       }
+
+       /**
+        * Creates a form widget in the provided parent. Note that this widget does
+        * not scroll its content, so make sure there is a scrolled composite up the
+        * parent chain. If you require scrolling, use 'createScrolledForm' instead.
+        *
+        * @param parent
+        *            the form parent
+        * @return the form that does not scroll
+        * @see #createScrolledForm
+        */
+//     public Form createForm(Composite parent) {
+//             Form formContent = new Form(parent, orientation);
+//             formContent.setBackground(colors.getBackground());
+//             formContent.setForeground(colors.getColor(IFormColors.TITLE));
+//             formContent.setFont(JFaceResources.getHeaderFont());
+//             return formContent;
+//     }
+
+       /**
+        * Takes advantage of the gradients and other capabilities to decorate the
+        * form heading using colors computed based on the current skin and
+        * operating system.
+        *
+        * @param form
+        *            the form to decorate
+        */
+
+//     public void decorateFormHeading(Form form) {
+//             Color top = colors.getColor(IFormColors.H_GRADIENT_END);
+//             Color bot = colors.getColor(IFormColors.H_GRADIENT_START);
+//             form.setTextBackground(new Color[] { top, bot }, new int[] { 100 },
+//                             true);
+//             form.setHeadColor(IFormColors.H_BOTTOM_KEYLINE1, colors
+//                             .getColor(IFormColors.H_BOTTOM_KEYLINE1));
+//             form.setHeadColor(IFormColors.H_BOTTOM_KEYLINE2, colors
+//                             .getColor(IFormColors.H_BOTTOM_KEYLINE2));
+//             form.setHeadColor(IFormColors.H_HOVER_LIGHT, colors
+//                             .getColor(IFormColors.H_HOVER_LIGHT));
+//             form.setHeadColor(IFormColors.H_HOVER_FULL, colors
+//                             .getColor(IFormColors.H_HOVER_FULL));
+//             form.setHeadColor(IFormColors.TB_TOGGLE, colors
+//                             .getColor(IFormColors.TB_TOGGLE));
+//             form.setHeadColor(IFormColors.TB_TOGGLE_HOVER, colors
+//                             .getColor(IFormColors.TB_TOGGLE_HOVER));
+//             form.setSeparatorVisible(true);
+//     }
+
+       /**
+        * Creates a scrolled page book widget as a part of the form.
+        *
+        * @param parent
+        *            the page book parent
+        * @param style
+        *            the text style
+        * @return the scrolled page book widget
+        */
+//     public ScrolledPageBook createPageBook(Composite parent, int style) {
+//             ScrolledPageBook book = new ScrolledPageBook(parent, style
+//                             | orientation);
+//             adapt(book, true, true);
+//             book.setMenu(parent.getMenu());
+//             return book;
+//     }
+
+       /**
+        * Disposes the toolkit.
+        */
+       public void dispose() {
+               if (isDisposed) {
+                       return;
+               }
+               isDisposed = true;
+               if (colors.isShared() == false) {
+                       colors.dispose();
+                       colors = null;
+               }
+               boldFontHolder.dispose();
+       }
+
+       /**
+        * Returns the hyperlink group that manages hyperlinks for this toolkit.
+        *
+        * @return the hyperlink group
+        */
+//     public HyperlinkGroup getHyperlinkGroup() {
+//             return hyperlinkGroup;
+//     }
+
+       /**
+        * Sets the background color for the entire toolkit. The method delegates
+        * the call to the FormColors object and also updates the hyperlink group so
+        * that hyperlinks and other objects are in sync.
+        *
+        * @param bg
+        *            the new background color
+        */
+       public void setBackground(Color bg) {
+//             hyperlinkGroup.setBackground(bg);
+               colors.setBackground(bg);
+       }
+
+       /**
+        * Refreshes the hyperlink colors by loading from JFace settings.
+        */
+//     public void refreshHyperlinkColors() {
+//             hyperlinkGroup.initializeDefaultForegrounds(colors.getDisplay());
+//     }
+
+// RAP [rh] paintBordersFor not useful as no GC to actually paint borders
+//     /**
+//      * Paints flat borders for widgets created by this toolkit within the
+//      * provided parent. Borders will not be painted if the global border style
+//      * is SWT.BORDER (i.e. if native borders are used). Call this method during
+//      * creation of a form composite to get the borders of its children painted.
+//      * Care should be taken when selection layout margins. At least one pixel
+//      * pargin width and height must be chosen to allow the toolkit to paint the
+//      * border on the parent around the widgets.
+//      * <p>
+//      * Borders are painted for some controls that are selected by the toolkit by
+//      * default. If a control needs a border but is not on its list, it is
+//      * possible to force border in the following way:
+//      *
+//      * <pre>
+//      *
+//      *
+//      *
+//      *             widget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER);
+//      *
+//      *             or
+//      *
+//      *             widget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TEXT_BORDER);
+//      *
+//      *
+//      *
+//      * </pre>
+//      *
+//      * @param parent
+//      *            the parent that owns the children for which the border needs
+//      *            to be painted.
+//      */
+//     public void paintBordersFor(Composite parent) {
+//             // if (borderStyle == SWT.BORDER)
+//             // return;
+//             if (borderPainter == null)
+//                     borderPainter = new BorderPainter();
+//             parent.addPaintListener(borderPainter);
+//     }
+
+       /**
+        * Returns the colors used by this toolkit.
+        *
+        * @return the color object
+        */
+       public FormColors getColors() {
+               return colors;
+       }
+
+       /**
+        * Returns the border style used for various widgets created by this
+        * toolkit. The intent of the toolkit is to create controls with styles that
+        * yield a 'flat' appearance. On systems where the native borders are
+        * already flat, we set the style to SWT.BORDER and don't paint the borders
+        * ourselves. Otherwise, the style is set to SWT.NULL, and borders are
+        * painted by the toolkit.
+        *
+        * @return the global border style
+        */
+       public int getBorderStyle() {
+               return borderStyle;
+       }
+
+       /**
+        * Returns the margin required around the children whose border is being
+        * painted by the toolkit using {@link #paintBordersFor(Composite)}. Since
+        * the border is painted around the controls on the parent, a number of
+        * pixels needs to be reserved for this border. For windowing systems where
+        * the native border is used, this margin is 0.
+        *
+        * @return the margin in the parent when children have their border painted
+        */
+       public int getBorderMargin() {
+               return getBorderStyle() == SWT.BORDER ? 0 : 2;
+       }
+
+       /**
+        * Sets the border style to be used when creating widgets. The toolkit
+        * chooses the correct style based on the platform but this value can be
+        * changed using this method.
+        *
+        * @param style
+        *            <code>SWT.BORDER</code> or <code>SWT.NULL</code>
+        * @see #getBorderStyle
+        */
+       public void setBorderStyle(int style) {
+               this.borderStyle = style;
+       }
+
+       /**
+        * A utility method that ensures that the control is visible in the scrolled
+        * composite. The prerequisite for this method is that the control has a
+        * class that extends ScrolledComposite somewhere in the parent chain. If
+        * the control is partially or fully clipped, the composite is scrolled to
+        * set by setting the origin to the control origin.
+        *
+        * @param c
+        *            the control to make visible
+        * @param verticalOnly
+        *            if <code>true</code>, the scrolled composite will be
+        *            scrolled only vertically if needed. Otherwise, the scrolled
+        *            composite origin will be set to the control origin.
+        */
+       public static void setControlVisible(Control c, boolean verticalOnly) {
+               ScrolledComposite scomp = FormUtil.getScrolledComposite(c);
+               if (scomp == null)
+                       return;
+               Point location = FormUtil.getControlLocation(scomp, c);
+               scomp.setOrigin(location);
+       }
+
+       private void initialize() {
+               initializeBorderStyle();
+//             hyperlinkGroup = new HyperlinkGroup(colors.getDisplay());
+//             hyperlinkGroup.setBackground(colors.getBackground());
+               visibilityHandler = new VisibilityHandler();
+               keyboardHandler = new KeyboardHandler();
+               boldFontHolder = new BoldFontHolder();
+       }
+
+// RAP [rh] revise detection of border style: can't ask OS here
+       private void initializeBorderStyle() {
+//             String osname = System.getProperty("os.name"); //$NON-NLS-1$
+//             String osversion = System.getProperty("os.version"); //$NON-NLS-1$
+//             if (osname.startsWith("Windows") && "5.1".compareTo(osversion) <= 0) { //$NON-NLS-1$ //$NON-NLS-2$
+//                     // Skinned widgets used on newer Windows (e.g. XP (5.1), Vista
+//                     // (6.0))
+//                     // Check for Windows Classic. If not used, set the style to BORDER
+//                     RGB rgb = colors.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);
+//                     if (rgb.red != 212 || rgb.green != 208 || rgb.blue != 200)
+//                             borderStyle = SWT.BORDER;
+//             } else if (osname.startsWith("Mac")) //$NON-NLS-1$
+//                     borderStyle = SWT.BORDER;
+
+               borderStyle = SWT.BORDER;
+       }
+
+       /**
+        * Returns the orientation that all the widgets created by this toolkit will
+        * inherit, if set. Can be <code>SWT.NULL</code>,
+        * <code>SWT.LEFT_TO_RIGHT</code> and <code>SWT.RIGHT_TO_LEFT</code>.
+        *
+        * @return orientation style for this toolkit, or <code>SWT.NULL</code> if
+        *         not set. The default orientation is inherited from the Window
+        *         default orientation.
+        * @see org.eclipse.jface.window.Window#getDefaultOrientation()
+        */
+
+       public int getOrientation() {
+               return orientation;
+       }
+
+       /**
+        * Sets the orientation that all the widgets created by this toolkit will
+        * inherit. Can be <code>SWT.NULL</code>, <code>SWT.LEFT_TO_RIGHT</code>
+        * and <code>SWT.RIGHT_TO_LEFT</code>.
+        *
+        * @param orientation
+        *            style for this toolkit.
+        */
+
+       public void setOrientation(int orientation) {
+               this.orientation = orientation;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java
new file mode 100644 (file)
index 0000000..76e3f11
--- /dev/null
@@ -0,0 +1,522 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.MouseEvent;
+//import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.FontMetrics;
+import org.eclipse.swt.graphics.GC;
+//import org.eclipse.swt.graphics.Image;
+//import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Layout;
+//import org.eclipse.swt.widgets.ScrollBar;
+import org.eclipse.swt.widgets.Text;
+//import org.eclipse.ui.forms.widgets.ColumnLayout;
+//import org.eclipse.ui.forms.widgets.Form;
+//import org.eclipse.ui.forms.widgets.FormText;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ILayoutExtension;
+//
+//import com.ibm.icu.text.BreakIterator;
+
+public class FormUtil {
+       public static final String PLUGIN_ID = "org.eclipse.ui.forms"; //$NON-NLS-1$
+
+       static final int H_SCROLL_INCREMENT = 5;
+
+       static final int V_SCROLL_INCREMENT = 64;
+
+       public static final String DEBUG = PLUGIN_ID + "/debug"; //$NON-NLS-1$
+
+       public static final String DEBUG_TEXT = DEBUG + "/text"; //$NON-NLS-1$
+       public static final String DEBUG_TEXTSIZE = DEBUG + "/textsize"; //$NON-NLS-1$
+
+       public static final String DEBUG_FOCUS = DEBUG + "/focus"; //$NON-NLS-1$
+
+       public static final String FOCUS_SCROLLING = "focusScrolling"; //$NON-NLS-1$
+       
+       public static final String IGNORE_BODY = "__ignore_body__"; //$NON-NLS-1$
+
+       public static Text createText(Composite parent, String label,
+                       FormToolkit factory) {
+               return createText(parent, label, factory, 1);
+       }
+
+       public static Text createText(Composite parent, String label,
+                       FormToolkit factory, int span) {
+               factory.createLabel(parent, label);
+               Text text = factory.createText(parent, ""); //$NON-NLS-1$
+               int hfill = span == 1 ? GridData.FILL_HORIZONTAL
+                               : GridData.HORIZONTAL_ALIGN_FILL;
+               GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER);
+               gd.horizontalSpan = span;
+               text.setLayoutData(gd);
+               return text;
+       }
+
+       public static Text createText(Composite parent, String label,
+                       FormToolkit factory, int span, int style) {
+               Label l = factory.createLabel(parent, label);
+               if ((style & SWT.MULTI) != 0) {
+                       GridData gd = new GridData(GridData.VERTICAL_ALIGN_BEGINNING);
+                       l.setLayoutData(gd);
+               }
+               Text text = factory.createText(parent, "", style); //$NON-NLS-1$
+               int hfill = span == 1 ? GridData.FILL_HORIZONTAL
+                               : GridData.HORIZONTAL_ALIGN_FILL;
+               GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER);
+               gd.horizontalSpan = span;
+               text.setLayoutData(gd);
+               return text;
+       }
+
+       public static Text createText(Composite parent, FormToolkit factory,
+                       int span) {
+               Text text = factory.createText(parent, ""); //$NON-NLS-1$
+               int hfill = span == 1 ? GridData.FILL_HORIZONTAL
+                               : GridData.HORIZONTAL_ALIGN_FILL;
+               GridData gd = new GridData(hfill | GridData.VERTICAL_ALIGN_CENTER);
+               gd.horizontalSpan = span;
+               text.setLayoutData(gd);
+               return text;
+       }
+
+       public static int computeMinimumWidth(GC gc, String text) {
+//             BreakIterator wb = BreakIterator.getWordInstance();
+//             wb.setText(text);
+//             int last = 0;
+//
+//             int width = 0;
+//
+//             for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) {
+//                     String word = text.substring(last, loc);
+//                     Point extent = gc.textExtent(word);
+//                     width = Math.max(width, extent.x);
+//                     last = loc;
+//             }
+//             String lastWord = text.substring(last);
+//             Point extent = gc.textExtent(lastWord);
+//             width = Math.max(width, extent.x);
+//             return width;
+               return 0;
+       }
+       
+       public static Point computeWrapSize(GC gc, String text, int wHint) {    
+//             BreakIterator wb = BreakIterator.getWordInstance();
+//             wb.setText(text);
+               FontMetrics fm = gc.getFontMetrics();
+               int lineHeight = fm.getHeight();
+               
+               int saved = 0;
+               int last = 0;
+               int height = lineHeight;
+               int maxWidth = 0;
+//             for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) {
+//                     String word = text.substring(saved, loc);
+//                     Point extent = gc.textExtent(word);
+//                     if (extent.x > wHint) {
+//                             // overflow
+//                             saved = last;
+//                             height += extent.y;
+//                             // switch to current word so maxWidth will accommodate very long single words
+//                             word = text.substring(last, loc);
+//                             extent = gc.textExtent(word);
+//                     }
+//                     maxWidth = Math.max(maxWidth, extent.x);
+//                     last = loc;
+//             }
+               /*
+                * Correct the height attribute in case it was calculated wrong due to wHint being less than maxWidth.
+                * The recursive call proved to be the only thing that worked in all cases. Some attempts can be made
+                * to estimate the height, but the algorithm needs to be run again to be sure.
+                */
+               if (maxWidth > wHint)
+                       return computeWrapSize(gc, text, maxWidth);               
+               return new Point(maxWidth, height);
+       }
+
+// RAP [rh] paintWrapText unnecessary
+//     public static void paintWrapText(GC gc, String text, Rectangle bounds) {
+//             paintWrapText(gc, text, bounds, false);
+//     }
+       
+// RAP [rh] paintWrapText unnecessary
+//     public static void paintWrapText(GC gc, String text, Rectangle bounds,
+//                     boolean underline) {
+//             BreakIterator wb = BreakIterator.getWordInstance();
+//             wb.setText(text);
+//             FontMetrics fm = gc.getFontMetrics();
+//             int lineHeight = fm.getHeight();
+//             int descent = fm.getDescent();
+//
+//             int saved = 0;
+//             int last = 0;
+//             int y = bounds.y;
+//             int width = bounds.width;
+//
+//             for (int loc = wb.first(); loc != BreakIterator.DONE; loc = wb.next()) {
+//                     String line = text.substring(saved, loc);
+//                     Point extent = gc.textExtent(line);
+//
+//                     if (extent.x > width) {
+//                             // overflow
+//                             String prevLine = text.substring(saved, last);
+//                             gc.drawText(prevLine, bounds.x, y, true);
+//                             if (underline) {
+//                                     Point prevExtent = gc.textExtent(prevLine);
+//                                     int lineY = y + lineHeight - descent + 1;
+//                                     gc
+//                                                     .drawLine(bounds.x, lineY, bounds.x + prevExtent.x,
+//                                                                     lineY);
+//                             }
+//
+//                             saved = last;
+//                             y += lineHeight;
+//                     }
+//                     last = loc;
+//             }
+//             // paint the last line
+//             String lastLine = text.substring(saved, last);
+//             gc.drawText(lastLine, bounds.x, y, true);
+//             if (underline) {
+//                     int lineY = y + lineHeight - descent + 1;
+//                     Point lastExtent = gc.textExtent(lastLine);
+//                     gc.drawLine(bounds.x, lineY, bounds.x + lastExtent.x, lineY);
+//             }
+//     }
+
+       public static ScrolledComposite getScrolledComposite(Control c) {
+               Composite parent = c.getParent();
+
+               while (parent != null) {
+                       if (parent instanceof ScrolledComposite) {
+                               return (ScrolledComposite) parent;
+                       }
+                       parent = parent.getParent();
+               }
+               return null;
+       }
+
+       public static void ensureVisible(Control c) {
+               ScrolledComposite scomp = getScrolledComposite(c);
+               if (scomp != null) {
+                       Object data = scomp.getData(FOCUS_SCROLLING);
+                       if (data == null || !data.equals(Boolean.FALSE))
+                               FormUtil.ensureVisible(scomp, c);
+               }
+       }
+
+       public static void ensureVisible(ScrolledComposite scomp, Control control) {
+               // if the control is a FormText we do not need to scroll since it will
+               // ensure visibility of its segments as necessary
+//             if (control instanceof FormText)
+//                     return;
+               Point controlSize = control.getSize();
+               Point controlOrigin = getControlLocation(scomp, control);
+               ensureVisible(scomp, controlOrigin, controlSize);
+       }
+
+       public static void ensureVisible(ScrolledComposite scomp,
+                       Point controlOrigin, Point controlSize) {
+               Rectangle area = scomp.getClientArea();
+               Point scompOrigin = scomp.getOrigin();
+
+               int x = scompOrigin.x;
+               int y = scompOrigin.y;
+
+               // horizontal right, but only if the control is smaller
+               // than the client area
+               if (controlSize.x < area.width
+                               && (controlOrigin.x + controlSize.x > scompOrigin.x
+                                               + area.width)) {
+                       x = controlOrigin.x + controlSize.x - area.width;
+               }
+               // horizontal left - make sure the left edge of
+               // the control is showing
+               if (controlOrigin.x < x) {
+                       if (controlSize.x < area.width)
+                               x = controlOrigin.x + controlSize.x - area.width;
+                       else
+                               x = controlOrigin.x;
+               }
+               // vertical bottom
+               if (controlSize.y < area.height
+                               && (controlOrigin.y + controlSize.y > scompOrigin.y
+                                               + area.height)) {
+                       y = controlOrigin.y + controlSize.y - area.height;
+               }
+               // vertical top - make sure the top of
+               // the control is showing
+               if (controlOrigin.y < y) {
+                       if (controlSize.y < area.height)
+                               y = controlOrigin.y + controlSize.y - area.height;
+                       else
+                               y = controlOrigin.y;
+               }
+
+               if (scompOrigin.x != x || scompOrigin.y != y) {
+                       // scroll to reveal
+                       scomp.setOrigin(x, y);
+               }
+       }
+
+       public static void ensureVisible(ScrolledComposite scomp, Control control,
+                       MouseEvent e) {
+               Point controlOrigin = getControlLocation(scomp, control);
+               int rX = controlOrigin.x + e.x;
+               int rY = controlOrigin.y + e.y;
+               Rectangle area = scomp.getClientArea();
+               Point scompOrigin = scomp.getOrigin();
+
+               int x = scompOrigin.x;
+               int y = scompOrigin.y;
+               // System.out.println("Ensure: area="+area+", origin="+scompOrigin+",
+               // cloc="+controlOrigin+", csize="+controlSize+", x="+x+", y="+y);
+
+               // horizontal right
+               if (rX > scompOrigin.x + area.width) {
+                       x = rX - area.width;
+               }
+               // horizontal left
+               else if (rX < x) {
+                       x = rX;
+               }
+               // vertical bottom
+               if (rY > scompOrigin.y + area.height) {
+                       y = rY - area.height;
+               }
+               // vertical top
+               else if (rY < y) {
+                       y = rY;
+               }
+
+               if (scompOrigin.x != x || scompOrigin.y != y) {
+                       // scroll to reveal
+                       scomp.setOrigin(x, y);
+               }
+       }
+
+       public static Point getControlLocation(ScrolledComposite scomp,
+                       Control control) {
+               int x = 0;
+               int y = 0;
+               Control content = scomp.getContent();
+               Control currentControl = control;
+               for (;;) {
+                       if (currentControl == content)
+                               break;
+                       Point location = currentControl.getLocation();
+                       // if (location.x > 0)
+                       // x += location.x;
+                       // if (location.y > 0)
+                       // y += location.y;
+                       x += location.x;
+                       y += location.y;
+                       currentControl = currentControl.getParent();
+               }
+               return new Point(x, y);
+       }
+
+       static void scrollVertical(ScrolledComposite scomp, boolean up) {
+               scroll(scomp, 0, up ? -V_SCROLL_INCREMENT : V_SCROLL_INCREMENT);
+       }
+
+       static void scrollHorizontal(ScrolledComposite scomp, boolean left) {
+               scroll(scomp, left ? -H_SCROLL_INCREMENT : H_SCROLL_INCREMENT, 0);
+       }
+
+       static void scrollPage(ScrolledComposite scomp, boolean up) {
+               Rectangle clientArea = scomp.getClientArea();
+               int increment = up ? -clientArea.height : clientArea.height;
+               scroll(scomp, 0, increment);
+       }
+
+       static void scroll(ScrolledComposite scomp, int xoffset, int yoffset) {
+               Point origin = scomp.getOrigin();
+               Point contentSize = scomp.getContent().getSize();
+               int xorigin = origin.x + xoffset;
+               int yorigin = origin.y + yoffset;
+               xorigin = Math.max(xorigin, 0);
+               xorigin = Math.min(xorigin, contentSize.x - 1);
+               yorigin = Math.max(yorigin, 0);
+               yorigin = Math.min(yorigin, contentSize.y - 1);
+               scomp.setOrigin(xorigin, yorigin);
+       }
+
+// RAP [rh] FormUtil#updatePageIncrement: empty implementation
+       public static void updatePageIncrement(ScrolledComposite scomp) {
+//             ScrollBar vbar = scomp.getVerticalBar();
+//             if (vbar != null) {
+//                     Rectangle clientArea = scomp.getClientArea();
+//                     int increment = clientArea.height - 5;
+//                     vbar.setPageIncrement(increment);
+//             }
+//             ScrollBar hbar = scomp.getHorizontalBar();
+//             if (hbar != null) {
+//                     Rectangle clientArea = scomp.getClientArea();
+//                     int increment = clientArea.width - 5;
+//                     hbar.setPageIncrement(increment);
+//             }
+       }
+
+       public static void processKey(int keyCode, Control c) {
+               if (c.isDisposed()) {
+                       return;
+               }
+               ScrolledComposite scomp = FormUtil.getScrolledComposite(c);
+               if (scomp != null) {
+                       if (c instanceof Combo)
+                               return;
+                       switch (keyCode) {
+                       case SWT.ARROW_DOWN:
+                               if (scomp.getData("novarrows") == null) //$NON-NLS-1$
+                                       FormUtil.scrollVertical(scomp, false);
+                               break;
+                       case SWT.ARROW_UP:
+                               if (scomp.getData("novarrows") == null) //$NON-NLS-1$
+                                       FormUtil.scrollVertical(scomp, true);
+                               break;
+                       case SWT.ARROW_LEFT:
+                               FormUtil.scrollHorizontal(scomp, true);
+                               break;
+                       case SWT.ARROW_RIGHT:
+                               FormUtil.scrollHorizontal(scomp, false);
+                               break;
+                       case SWT.PAGE_UP:
+                               FormUtil.scrollPage(scomp, true);
+                               break;
+                       case SWT.PAGE_DOWN:
+                               FormUtil.scrollPage(scomp, false);
+                               break;
+                       }
+               }
+       }
+
+       public static boolean isWrapControl(Control c) {
+               if ((c.getStyle() & SWT.WRAP) != 0)
+                       return true;
+               if (c instanceof Composite) {
+                       return false;
+//                     return ((Composite) c).getLayout() instanceof ILayoutExtension;
+               }
+               return false;
+       }
+
+       public static int getWidthHint(int wHint, Control c) {
+               boolean wrap = isWrapControl(c);
+               return wrap ? wHint : SWT.DEFAULT;
+       }
+
+       public static int getHeightHint(int hHint, Control c) {
+               if (c instanceof Composite) {
+                       Layout layout = ((Composite) c).getLayout();
+//                     if (layout instanceof ColumnLayout)
+//                             return hHint;
+               }
+               return SWT.DEFAULT;
+       }
+
+       public static int computeMinimumWidth(Control c, boolean changed) {
+               if (c instanceof Composite) {
+                       Layout layout = ((Composite) c).getLayout();
+//                     if (layout instanceof ILayoutExtension)
+//                             return ((ILayoutExtension) layout).computeMinimumWidth(
+//                                             (Composite) c, changed);
+               }
+               return c.computeSize(FormUtil.getWidthHint(5, c), SWT.DEFAULT, changed).x;
+       }
+
+       public static int computeMaximumWidth(Control c, boolean changed) {
+               if (c instanceof Composite) {
+                       Layout layout = ((Composite) c).getLayout();
+//                     if (layout instanceof ILayoutExtension)
+//                             return ((ILayoutExtension) layout).computeMaximumWidth(
+//                                             (Composite) c, changed);
+               }
+               return c.computeSize(SWT.DEFAULT, SWT.DEFAULT, changed).x;
+       }
+
+//     public static Form getForm(Control c) {
+//             Composite parent = c.getParent();
+//             while (parent != null) {
+//                     if (parent instanceof Form) {
+//                             return (Form) parent;
+//                     }
+//                     parent = parent.getParent();
+//             }
+//             return null;
+//     }
+
+// RAP [rh] FormUtil#createAlphaMashImage unnecessary  
+//     public static Image createAlphaMashImage(Device device, Image srcImage) {
+//             Rectangle bounds = srcImage.getBounds();
+//             int alpha = 0;
+//             int calpha = 0;
+//             ImageData data = srcImage.getImageData();
+//             // Create a new image with alpha values alternating
+//             // between fully transparent (0) and fully opaque (255).
+//             // This image will show the background through the
+//             // transparent pixels.
+//             for (int i = 0; i < bounds.height; i++) {
+//                     // scan line
+//                     alpha = calpha;
+//                     for (int j = 0; j < bounds.width; j++) {
+//                             // column
+//                             data.setAlpha(j, i, alpha);
+//                             alpha = alpha == 255 ? 0 : 255;
+//                     }
+//                     calpha = calpha == 255 ? 0 : 255;
+//             }
+//             return new Image(device, data);
+//     }
+
+       public static boolean mnemonicMatch(String text, char key) {
+               char mnemonic = findMnemonic(text);
+               if (mnemonic == '\0')
+                       return false;
+               return Character.toUpperCase(key) == Character.toUpperCase(mnemonic);
+       }
+
+       private static char findMnemonic(String string) {
+               int index = 0;
+               int length = string.length();
+               do {
+                       while (index < length && string.charAt(index) != '&')
+                               index++;
+                       if (++index >= length)
+                               return '\0';
+                       if (string.charAt(index) != '&')
+                               return string.charAt(index);
+                       index++;
+               } while (index < length);
+               return '\0';
+       }
+       
+       public static void setFocusScrollingEnabled(Control c, boolean enabled) {
+               ScrolledComposite scomp = null;
+               
+               if (c instanceof ScrolledComposite)
+                       scomp = (ScrolledComposite)c;
+               else
+                       scomp = getScrolledComposite(c);
+               if (scomp!=null)
+                       scomp.setData(FormUtil.FOCUS_SCROLLING, enabled?null:Boolean.FALSE);
+       }
+       
+       // RAP [rh] FormUtil#setAntialias unnecessary
+//     public static void setAntialias(GC gc, int style) {
+//             if (!gc.getAdvanced()) {
+//                     gc.setAdvanced(true);
+//                     if (!gc.getAdvanced())
+//                             return;
+//             }
+//             gc.setAntialias(style);
+//     }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java
new file mode 100644 (file)
index 0000000..cf0e5d3
--- /dev/null
@@ -0,0 +1,102 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+/**
+ * A place to hold all the color constants used in the forms package.
+ * 
+ * @since 1.0
+ */
+
+public interface IFormColors {
+       /**
+        * A prefix for all the keys.
+        */
+       String PREFIX = "org.eclipse.ui.forms."; //$NON-NLS-1$
+       /**
+        * Key for the form title foreground color.
+        */
+       String TITLE = PREFIX + "TITLE"; //$NON-NLS-1$
+
+       /**
+        * A prefix for the header color constants.
+        */
+       String H_PREFIX = PREFIX + "H_"; //$NON-NLS-1$
+       /*
+        * A prefix for the section title bar color constants.
+        */
+       String TB_PREFIX = PREFIX + "TB_"; //$NON-NLS-1$        
+       /**
+        * Key for the form header background gradient ending color.
+        */
+       String H_GRADIENT_END = H_PREFIX + "GRADIENT_END"; //$NON-NLS-1$
+
+       /**
+        * Key for the form header background gradient starting color.
+        * 
+        */
+       String H_GRADIENT_START = H_PREFIX + "GRADIENT_START"; //$NON-NLS-1$
+       /**
+        * Key for the form header bottom keyline 1 color.
+        * 
+        */
+       String H_BOTTOM_KEYLINE1 = H_PREFIX + "BOTTOM_KEYLINE1"; //$NON-NLS-1$
+       /**
+        * Key for the form header bottom keyline 2 color.
+        * 
+        */
+       String H_BOTTOM_KEYLINE2 = H_PREFIX + "BOTTOM_KEYLINE2"; //$NON-NLS-1$
+       /**
+        * Key for the form header light hover color.
+        * 
+        */
+       String H_HOVER_LIGHT = H_PREFIX + "H_HOVER_LIGHT"; //$NON-NLS-1$
+       /**
+        * Key for the form header full hover color.
+        * 
+        */
+       String H_HOVER_FULL = H_PREFIX + "H_HOVER_FULL"; //$NON-NLS-1$
+
+       /**
+        * Key for the tree/table border color.
+        */
+       String BORDER = PREFIX + "BORDER"; //$NON-NLS-1$
+
+       /**
+        * Key for the section separator color.
+        */
+       String SEPARATOR = PREFIX + "SEPARATOR"; //$NON-NLS-1$
+
+       /**
+        * Key for the section title bar background.
+        */
+       String TB_BG = TB_PREFIX + "BG"; //$NON-NLS-1$
+
+       /**
+        * Key for the section title bar foreground.
+        */
+       String TB_FG = TB_PREFIX + "FG"; //$NON-NLS-1$
+
+       /**
+        * Key for the section title bar gradient.
+        * @deprecated Since 3.3, this color is not used any more. The 
+        * tool bar gradient is created starting from {@link #TB_BG} to
+        * the section background color.
+        */
+       String TB_GBG = TB_BG;
+
+       /**
+        * Key for the section title bar border.
+        */
+       String TB_BORDER = TB_PREFIX + "BORDER"; //$NON-NLS-1$
+
+       /**
+        * Key for the section toggle color. Since 3.1, this color is used for all
+        * section styles.
+        */
+       String TB_TOGGLE = TB_PREFIX + "TOGGLE"; //$NON-NLS-1$
+
+       /**
+        * Key for the section toggle hover color.
+        * 
+        */
+       String TB_TOGGLE_HOVER = TB_PREFIX + "TOGGLE_HOVER"; //$NON-NLS-1$              
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java
new file mode 100644 (file)
index 0000000..954cc03
--- /dev/null
@@ -0,0 +1,108 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+/**
+ * Classes that implement this interface can be added to the managed form and
+ * take part in the form life cycle. The part is initialized with the form and
+ * will be asked to accept focus. The part can receive form input and can elect
+ * to do something according to it (for example, select an object that matches
+ * the input).
+ * <p>
+ * The form part has two 'out of sync' states in respect to the model(s) that
+ * feed the form: <b>dirty</b> and <b>stale</b>. When a part is dirty, it
+ * means that the user interacted with it and now its widgets contain state that
+ * is newer than the model. In order to sync up with the model, 'commit' needs
+ * to be called. In contrast, the model can change 'under' the form (as a result
+ * of some actions outside the form), resulting in data in the model being
+ * 'newer' than the content presented in the form. A 'stale' form part is
+ * brought in sync with the model by calling 'refresh'. The part is responsible
+ * for notifying the form when one of these states change in the part. The form
+ * reserves the right to handle this notification in the most appropriate way
+ * for the situation (for example, if the form is in a page of the multi-page
+ * editor, it may do nothing for stale parts if the page is currently not
+ * showing).
+ * <p>
+ * When the form is disposed, each registered part is disposed as well. Parts
+ * are responsible for releasing any system resources they created and for
+ * removing themselves as listeners from all event providers.
+ * 
+ * @see IManagedForm
+ * @since 1.0
+ * 
+ */
+public interface IFormPart {
+       /**
+        * Initializes the part.
+        * 
+        * @param form
+        *            the managed form that manages the part
+        */
+       void initialize(IManagedForm form);
+
+       /**
+        * Disposes the part allowing it to release allocated resources.
+        */
+       void dispose();
+
+       /**
+        * Returns true if the part has been modified with respect to the data
+        * loaded from the model.
+        * 
+        * @return true if the part has been modified with respect to the data
+        *         loaded from the model
+        */
+       boolean isDirty();
+
+       /**
+        * If part is displaying information loaded from a model, this method
+        * instructs it to commit the new (modified) data back into the model.
+        * 
+        * @param onSave
+        *            indicates if commit is called during 'save' operation or for
+        *            some other reason (for example, if form is contained in a
+        *            wizard or a multi-page editor and the user is about to leave
+        *            the page).
+        */
+       void commit(boolean onSave);
+
+       /**
+        * Notifies the part that an object has been set as overall form's input.
+        * The part can elect to react by revealing or selecting the object, or do
+        * nothing if not applicable.
+        * 
+        * @return <code>true</code> if the part has selected and revealed the
+        *         input object, <code>false</code> otherwise.
+        */
+       boolean setFormInput(Object input);
+
+       /**
+        * Instructs form part to transfer focus to the widget that should has focus
+        * in that part. The method can do nothing (if it has no widgets capable of
+        * accepting focus).
+        */
+       void setFocus();
+
+       /**
+        * Tests whether the form part is stale and needs refreshing. Parts can
+        * receive notification from models that will make their content stale, but
+        * may need to delay refreshing to improve performance (for example, there
+        * is no need to immediately refresh a part on a form that is current on a
+        * hidden page).
+        * <p>
+        * It is important to differentiate 'stale' and 'dirty' states. Part is
+        * 'dirty' if user interacted with its editable widgets and changed the
+        * values. In contrast, part is 'stale' when the data it presents in the
+        * widgets has been changed in the model without direct user interaction.
+        * 
+        * @return <code>true</code> if the part needs refreshing,
+        *         <code>false</code> otherwise.
+        */
+       boolean isStale();
+
+       /**
+        * Refreshes the part completely from the information freshly obtained from
+        * the model. The method will not be called if the part is not stale.
+        * Otherwise, the part is responsible for clearing the 'stale' flag after
+        * refreshing itself.
+        */
+       void refresh();
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java
new file mode 100644 (file)
index 0000000..490d3a3
--- /dev/null
@@ -0,0 +1,175 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.swt.custom.ScrolledComposite;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Managed form wraps a form widget and adds life cycle methods for form parts.
+ * A form part is a portion of the form that participates in form life cycle
+ * events.
+ * <p>
+ * There is no 1/1 mapping between widgets and form parts. A widget like Section
+ * can be a part by itself, but a number of widgets can gather around one form
+ * part.
+ * <p>
+ * This interface should not be extended or implemented. New form instances
+ * should be created using ManagedForm.
+ * 
+ * @see ManagedForm
+ * @since 1.0
+ * @noimplement This interface is not intended to be implemented by clients.
+ * @noextend This interface is not intended to be extended by clients.
+ */
+public interface IManagedForm {
+       /**
+        * Initializes the form by looping through the managed parts and
+        * initializing them. Has no effect if already called once.
+        */
+       public void initialize();
+
+       /**
+        * Returns the toolkit used by this form.
+        * 
+        * @return the toolkit
+        */
+       public FormToolkit getToolkit();
+
+       /**
+        * Returns the form widget managed by this form.
+        * 
+        * @return the form widget
+        */
+       public ScrolledComposite getForm();
+
+       /**
+        * Reflows the form as a result of the layout change.
+        * 
+        * @param changed
+        *            if <code>true</code>, discard cached layout information
+        */
+       public void reflow(boolean changed);
+
+       /**
+        * A part can use this method to notify other parts that implement
+        * IPartSelectionListener about selection changes.
+        * 
+        * @param part
+        *            the part that broadcasts the selection
+        * @param selection
+        *            the selection in the part
+        */
+       public void fireSelectionChanged(IFormPart part, ISelection selection);
+
+       /**
+        * Returns all the parts currently managed by this form.
+        * 
+        * @return the managed parts
+        */
+       IFormPart[] getParts();
+
+       /**
+        * Adds the new part to the form.
+        * 
+        * @param part
+        *            the part to add
+        */
+       void addPart(IFormPart part);
+
+       /**
+        * Removes the part from the form.
+        * 
+        * @param part
+        *            the part to remove
+        */
+       void removePart(IFormPart part);
+
+       /**
+        * Sets the input of this page to the provided object.
+        * 
+        * @param input
+        *            the new page input
+        * @return <code>true</code> if the form contains this object,
+        *         <code>false</code> otherwise.
+        */
+       boolean setInput(Object input);
+
+       /**
+        * Returns the current page input.
+        * 
+        * @return page input object or <code>null</code> if not applicable.
+        */
+       Object getInput();
+
+       /**
+        * Tests if form is dirty. A managed form is dirty if at least one managed
+        * part is dirty.
+        * 
+        * @return <code>true</code> if at least one managed part is dirty,
+        *         <code>false</code> otherwise.
+        */
+       boolean isDirty();
+
+       /**
+        * Notifies the form that the dirty state of one of its parts has changed.
+        * The global dirty state of the form can be obtained by calling 'isDirty'.
+        * 
+        * @see #isDirty
+        */
+       void dirtyStateChanged();
+
+       /**
+        * Commits the dirty form. All pending changes in the widgets are flushed
+        * into the model.
+        * 
+        * @param onSave
+        */
+       void commit(boolean onSave);
+
+       /**
+        * Tests if form is stale. A managed form is stale if at least one managed
+        * part is stale. This can happen when the underlying model changes,
+        * resulting in the presentation of the part being out of sync with the
+        * model and needing refreshing.
+        * 
+        * @return <code>true</code> if the form is stale, <code>false</code>
+        *         otherwise.
+        */
+       boolean isStale();
+
+       /**
+        * Notifies the form that the stale state of one of its parts has changed.
+        * The global stale state of the form can be obtained by calling 'isStale'.
+        */
+       void staleStateChanged();
+
+       /**
+        * Refreshes the form by refreshing every part that is stale.
+        */
+       void refresh();
+
+       /**
+        * Sets the container that owns this form. Depending on the context, the
+        * container may be wizard, editor page, editor etc.
+        * 
+        * @param container
+        *            the container of this form
+        */
+       void setContainer(Object container);
+
+       /**
+        * Returns the container of this form.
+        * 
+        * @return the form container
+        */
+       Object getContainer();
+
+       /**
+        * Returns the message manager that will keep track of messages in this
+        * form.
+        * 
+        * @return the message manager instance
+        */
+//     IMessageManager getMessageManager();
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java
new file mode 100644 (file)
index 0000000..0f557d4
--- /dev/null
@@ -0,0 +1,23 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import org.eclipse.jface.viewers.ISelection;
+
+/**
+ * Form parts can implement this interface if they want to be 
+ * notified when another part on the same form changes selection 
+ * state.
+ * 
+ * @see IFormPart
+ * @since 1.0
+ */
+public interface IPartSelectionListener {
+       /**
+        * Called when the provided part has changed selection state.
+        * 
+        * @param part
+        *            the selection source
+        * @param selection
+        *            the new selection
+        */
+       public void selectionChanged(IFormPart part, ISelection selection);
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java
new file mode 100644 (file)
index 0000000..4140465
--- /dev/null
@@ -0,0 +1,323 @@
+package org.argeo.cms.ui.eclipse.forms;
+
+import java.util.Vector;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.widgets.Composite;
+//import org.eclipse.ui.forms.widgets.FormToolkit;
+//import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Managed form wraps a form widget and adds life cycle methods for form parts.
+ * A form part is a portion of the form that participates in form life cycle
+ * events.
+ * <p>
+ * There is requirement for 1/1 mapping between widgets and form parts. A widget
+ * like Section can be a part by itself, but a number of widgets can join around
+ * one form part.
+ * <p>
+ * Note to developers: this class is left public to allow its use beyond the
+ * original intention (inside a multi-page editor's page). You should limit the
+ * use of this class to make new instances inside a form container (wizard page,
+ * dialog etc.). Clients that need access to the class should not do it
+ * directly. Instead, they should do it through IManagedForm interface as much
+ * as possible.
+ * 
+ * @since 1.0
+ */
+public class ManagedForm implements IManagedForm {
+       private Object input;
+
+       private ScrolledComposite form;
+
+       private FormToolkit toolkit;
+
+       private Object container;
+
+       private boolean ownsToolkit;
+
+       private boolean initialized;
+
+       private Vector parts = new Vector();
+
+       /**
+        * Creates a managed form in the provided parent. Form toolkit and widget
+        * will be created and owned by this object.
+        * 
+        * @param parent
+        *            the parent widget
+        */
+       public ManagedForm(Composite parent) {
+               toolkit = new FormToolkit(parent.getDisplay());
+               ownsToolkit = true;
+               form = toolkit.createScrolledForm(parent);
+               
+       }
+
+       /**
+        * Creates a managed form that will use the provided toolkit and
+        * 
+        * @param toolkit
+        * @param form
+        */
+       public ManagedForm(FormToolkit toolkit, ScrolledComposite form) {
+               this.form = form;
+               this.toolkit = toolkit;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#addPart(org.eclipse.ui.forms.IFormPart)
+        */
+       public void addPart(IFormPart part) {
+               parts.add(part);
+               part.initialize(this);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#removePart(org.eclipse.ui.forms.IFormPart)
+        */
+       public void removePart(IFormPart part) {
+               parts.remove(part);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#getParts()
+        */
+       public IFormPart[] getParts() {
+               return (IFormPart[]) parts.toArray(new IFormPart[parts.size()]);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#getToolkit()
+        */
+       public FormToolkit getToolkit() {
+               return toolkit;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#getForm()
+        */
+       public ScrolledComposite getForm() {
+               return form;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#reflow(boolean)
+        */
+       public void reflow(boolean changed) {
+//             form.reflow(changed);
+       }
+
+       /**
+        * A part can use this method to notify other parts that implement
+        * IPartSelectionListener about selection changes.
+        * 
+        * @param part
+        *            the part that broadcasts the selection
+        * @param selection
+        *            the selection in the part
+        * @see IPartSelectionListener
+        */
+       public void fireSelectionChanged(IFormPart part, ISelection selection) {
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart cpart = (IFormPart) parts.get(i);
+                       if (part.equals(cpart))
+                               continue;
+//                     if (cpart instanceof IPartSelectionListener) {
+//                             ((IPartSelectionListener) cpart).selectionChanged(part,
+//                                             selection);
+//                     }
+               }
+       }
+
+       /**
+        * Initializes the form by looping through the managed parts and
+        * initializing them. Has no effect if already called once.
+        */
+       public void initialize() {
+               if (initialized)
+                       return;
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       part.initialize(this);
+               }
+               initialized = true;
+       }
+
+       /**
+        * Disposes all the parts in this form.
+        */
+       public void dispose() {
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       part.dispose();
+               }
+               if (ownsToolkit) {
+                       toolkit.dispose();
+               }
+       }
+
+       /**
+        * Refreshes the form by refreshes all the stale parts. Since 3.1, this
+        * method is performed on a UI thread when called from another thread so it
+        * is not needed to wrap the call in <code>Display.syncExec</code> or
+        * <code>asyncExec</code>.
+        */
+       public void refresh() {
+               Thread t = Thread.currentThread();
+               Thread dt = toolkit.getColors().getDisplay().getThread();
+               if (t.equals(dt))
+                       doRefresh();
+               else {
+                       toolkit.getColors().getDisplay().asyncExec(new Runnable() {
+                               public void run() {
+                                       doRefresh();
+                               }
+                       });
+               }
+       }
+
+       private void doRefresh() {
+               int nrefreshed = 0;
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       if (part.isStale()) {
+                               part.refresh();
+                               nrefreshed++;
+                       }
+               }
+//             if (nrefreshed > 0)
+//                     form.reflow(true);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#commit(boolean)
+        */
+       public void commit(boolean onSave) {
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       if (part.isDirty())
+                               part.commit(onSave);
+               }
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#setInput(java.lang.Object)
+        */
+       public boolean setInput(Object input) {
+               boolean pageResult = false;
+
+               this.input = input;
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       boolean result = part.setFormInput(input);
+                       if (result)
+                               pageResult = true;
+               }
+               return pageResult;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#getInput()
+        */
+       public Object getInput() {
+               return input;
+       }
+
+       /**
+        * Transfers the focus to the first form part.
+        */
+       public void setFocus() {
+               if (parts.size() > 0) {
+                       IFormPart part = (IFormPart) parts.get(0);
+                       part.setFocus();
+               }
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#isDirty()
+        */
+       public boolean isDirty() {
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       if (part.isDirty())
+                               return true;
+               }
+               return false;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#isStale()
+        */
+       public boolean isStale() {
+               for (int i = 0; i < parts.size(); i++) {
+                       IFormPart part = (IFormPart) parts.get(i);
+                       if (part.isStale())
+                               return true;
+               }
+               return false;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#dirtyStateChanged()
+        */
+       public void dirtyStateChanged() {
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#staleStateChanged()
+        */
+       public void staleStateChanged() {
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#getContainer()
+        */
+       public Object getContainer() {
+               return container;
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see org.eclipse.ui.forms.IManagedForm#setContainer(java.lang.Object)
+        */
+       public void setContainer(Object container) {
+               this.container = container;
+       }
+
+       /* (non-Javadoc)
+        * @see org.eclipse.ui.forms.IManagedForm#getMessageManager()
+        */
+//     public IMessageManager getMessageManager() {
+//             return form.getMessageManager();
+//     }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java
new file mode 100644 (file)
index 0000000..7fa00d9
--- /dev/null
@@ -0,0 +1,89 @@
+package org.argeo.cms.ui.eclipse.forms.editor;
+
+import org.argeo.cms.ui.eclipse.forms.FormToolkit;
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.jface.dialogs.IPageChangeProvider;
+import org.eclipse.jface.dialogs.IPageChangedListener;
+import org.eclipse.jface.dialogs.PageChangedEvent;
+import org.eclipse.jface.util.SafeRunnable;
+
+/**
+ * This class forms a base of multi-page form editors that typically use one or
+ * more pages with forms and one page for raw source of the editor input.
+ * <p>
+ * Pages are added 'lazily' i.e. adding a page reserves a tab for it but does
+ * not cause the page control to be created. Page control is created when an
+ * attempt is made to select the page in question. This allows editors with
+ * several tabs and complex pages to open quickly.
+ * <p>
+ * Subclasses should extend this class and implement <code>addPages</code>
+ * method. One of the two <code>addPage</code> methods should be called to
+ * contribute pages to the editor. One adds complete (standalone) editors as
+ * nested tabs. These editors will be created right away and will be hooked so
+ * that key bindings, selection service etc. is compatible with the one for the
+ * standalone case. The other method adds classes that implement
+ * <code>IFormPage</code> interface. These pages will be created lazily and
+ * they will share the common key binding and selection service. Since 3.1,
+ * FormEditor is a page change provider. It allows listeners to attach to it and
+ * get notified when pages are changed. This new API in JFace allows dynamic
+ * help to update on page changes.
+ * 
+ * @since 1.0
+ */
+// RAP [if] As RAP is still using workbench 3.4, the implementation of
+// IPageChangeProvider is missing from MultiPageEditorPart. Remove this code
+// with the adoption of workbench > 3.5
+//public abstract class FormEditor extends MultiPageEditorPart  {
+public abstract class FormEditor  implements
+        IPageChangeProvider {
+       private FormToolkit formToolkit;
+       
+       
+public FormToolkit getToolkit() {
+               return formToolkit;
+       }
+
+public void editorDirtyStateChanged() {
+       
+}
+
+public FormPage getActivePageInstance() {
+       return null;
+}
+
+       // RAP [if] As RAP is still using workbench 3.4, the implementation of
+// IPageChangeProvider is missing from MultiPageEditorPart. Remove this code
+// with the adoption of workbench > 3.5
+       private ListenerList pageListeners = new ListenerList();
+       
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.eclipse.jface.dialogs.IPageChangeProvider#addPageChangedListener(org.eclipse.jface.dialogs.IPageChangedListener)
+     */
+    public void addPageChangedListener(IPageChangedListener listener) {
+        pageListeners.add(listener);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see org.eclipse.jface.dialogs.IPageChangeProvider#removePageChangedListener(org.eclipse.jface.dialogs.IPageChangedListener)
+     */
+    public void removePageChangedListener(IPageChangedListener listener) {
+        pageListeners.remove(listener);
+    }
+    
+       private void firePageChanged(final PageChangedEvent event) {
+        Object[] listeners = pageListeners.getListeners();
+        for (int i = 0; i < listeners.length; ++i) {
+            final IPageChangedListener l = (IPageChangedListener) listeners[i];
+            SafeRunnable.run(new SafeRunnable() {
+                public void run() {
+                    l.pageChanged(event);
+                }
+            });
+        }
+    }
+// RAPEND [if]
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java
new file mode 100644 (file)
index 0000000..1511cf3
--- /dev/null
@@ -0,0 +1,277 @@
+package org.argeo.cms.ui.eclipse.forms.editor;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.argeo.cms.ui.eclipse.forms.ManagedForm;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.swt.custom.BusyIndicator;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+/**
+ * A base class that all pages that should be added to FormEditor must subclass.
+ * Form page has an instance of PageForm that extends managed form. Subclasses
+ * should override method 'createFormContent(ManagedForm)' to fill the form with
+ * content. Note that page itself can be loaded lazily (on first open).
+ * Consequently, the call to create the form content can come after the editor
+ * has been opened for a while (in fact, it is possible to open and close the
+ * editor and never create the form because no attempt has been made to show the
+ * page).
+ * 
+ * @since 1.0
+ */
+public class FormPage implements IFormPage {
+       private FormEditor editor;
+       private PageForm mform;
+       private int index;
+       private String id;
+       
+       private String partName;
+       
+       
+       
+       public void setPartName(String partName) {
+               this.partName = partName;
+       }
+       private static class PageForm extends ManagedForm {
+               public PageForm(FormPage page, ScrolledComposite form) {
+                       super(page.getEditor().getToolkit(), form);
+                       setContainer(page);
+               }
+               
+               public FormPage getPage() {
+                       return (FormPage)getContainer();
+               }
+               public void dirtyStateChanged() {
+                       getPage().getEditor().editorDirtyStateChanged();
+               }
+               public void staleStateChanged() {
+                       if (getPage().isActive())
+                               refresh();
+               }
+       }
+       /**
+        * A constructor that creates the page and initializes it with the editor.
+        * 
+        * @param editor
+        *            the parent editor
+        * @param id
+        *            the unique identifier
+        * @param title
+        *            the page title
+        */
+       public FormPage(FormEditor editor, String id, String title) {
+               this(id, title);
+               initialize(editor);
+       }
+       /**
+        * The constructor. The parent editor need to be passed in the
+        * <code>initialize</code> method if this constructor is used.
+        * 
+        * @param id
+        *            a unique page identifier
+        * @param title
+        *            a user-friendly page title
+        */
+       public FormPage(String id, String title) {
+               this.id = id;
+               setPartName(title);
+       }
+       /**
+        * Initializes the form page.
+        * 
+        * @see IEditorPart#init
+        */
+//     public void init(IEditorSite site, IEditorInput input) {
+//             setSite(site);
+//             setInput(input);
+//     }
+       /**
+        * Primes the form page with the parent editor instance.
+        * 
+        * @param editor
+        *            the parent editor
+        */
+       public void initialize(FormEditor editor) {
+               this.editor = editor;
+       }
+       /**
+        * Returns the parent editor.
+        * 
+        * @return parent editor instance
+        */
+       public FormEditor getEditor() {
+               return editor;
+       }
+       /**
+        * Returns the managed form owned by this page.
+        * 
+        * @return the managed form
+        */
+       public IManagedForm getManagedForm() {
+               return mform;
+       }
+       /**
+        * Implements the required method by refreshing the form when set active.
+        * Subclasses must call super when overriding this method.
+        */
+       public void setActive(boolean active) {
+               if (active) {
+                       // We are switching to this page - refresh it
+                       // if needed.
+                       if (mform != null)
+                               mform.refresh();
+               }
+       }
+       /**
+        * Tests if the page is active by asking the parent editor if this page is
+        * the currently active page.
+        * 
+        * @return <code>true</code> if the page is currently active,
+        *         <code>false</code> otherwise.
+        */
+       public boolean isActive() {
+               return this.equals(editor.getActivePageInstance());
+       }
+       /**
+        * Creates the part control by creating the managed form using the parent
+        * editor's toolkit. Subclasses should override
+        * <code>createFormContent(IManagedForm)</code> to populate the form with
+        * content.
+        * 
+        * @param parent
+        *            the page parent composite
+        */
+       public void createPartControl(Composite parent) {
+               ScrolledComposite form = editor.getToolkit().createScrolledForm(parent);
+               mform = new PageForm(this, form);
+               BusyIndicator.showWhile(parent.getDisplay(), new Runnable() {
+                       public void run() {
+                               createFormContent(mform);
+                       }
+               });
+       }
+       /**
+        * Subclasses should override this method to create content in the form
+        * hosted in this page.
+        * 
+        * @param managedForm
+        *            the form hosted in this page.
+        */
+       protected void createFormContent(IManagedForm managedForm) {
+       }
+       /**
+        * Returns the form page control.
+        * 
+        * @return managed form's control
+        */
+       public Control getPartControl() {
+               return mform != null ? mform.getForm() : null;
+       }
+       /**
+        * Disposes the managed form.
+        */
+       public void dispose() {
+               if (mform != null)
+                       mform.dispose();
+       }
+       /**
+        * Returns the unique identifier that can be used to reference this page.
+        * 
+        * @return the unique page identifier
+        */
+       public String getId() {
+               return id;
+       }
+       /**
+        * Returns <code>null</code>- form page has no title image. Subclasses
+        * may override.
+        * 
+        * @return <code>null</code>
+        */
+       public Image getTitleImage() {
+               return null;
+       }
+       /**
+        * Sets the focus by delegating to the managed form.
+        */
+       public void setFocus() {
+               if (mform != null)
+                       mform.setFocus();
+       }
+       /**
+        * @see org.eclipse.ui.ISaveablePart#doSave(org.eclipse.core.runtime.IProgressMonitor)
+        */
+       public void doSave(IProgressMonitor monitor) {
+               if (mform != null)
+                       mform.commit(true);
+       }
+       /**
+        * @see org.eclipse.ui.ISaveablePart#doSaveAs()
+        */
+       public void doSaveAs() {
+       }
+       /**
+        * @see org.eclipse.ui.ISaveablePart#isSaveAsAllowed()
+        */
+       public boolean isSaveAsAllowed() {
+               return false;
+       }
+       /**
+        * Implemented by testing if the managed form is dirty.
+        * 
+        * @return <code>true</code> if the managed form is dirty,
+        *         <code>false</code> otherwise.
+        * 
+        * @see org.eclipse.ui.ISaveablePart#isDirty()
+        */
+       public boolean isDirty() {
+               return mform != null ? mform.isDirty() : false;
+       }
+       /**
+        * Preserves the page index.
+        * 
+        * @param index
+        *            the assigned page index
+        */
+       public void setIndex(int index) {
+               this.index = index;
+       }
+       /**
+        * Returns the saved page index.
+        * 
+        * @return the page index
+        */
+       public int getIndex() {
+               return index;
+       }
+       /**
+        * Form pages are not editors.
+        * 
+        * @return <code>false</code>
+        */
+       public boolean isEditor() {
+               return false;
+       }
+       /**
+        * Attempts to select and reveal the given object by passing the request to
+        * the managed form.
+        * 
+        * @param object
+        *            the object to select and reveal in the page if possible.
+        * @return <code>true</code> if the page has been successfully selected
+        *         and revealed by one of the managed form parts, <code>false</code>
+        *         otherwise.
+        */
+       public boolean selectReveal(Object object) {
+               if (mform != null)
+                       return mform.setInput(object);
+               return false;
+       }
+       /**
+        * By default, editor will be allowed to flip the page.
+        * @return <code>true</code>
+        */
+       public boolean canLeaveThePage() {
+               return true;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java
new file mode 100644 (file)
index 0000000..eb08cb5
--- /dev/null
@@ -0,0 +1,119 @@
+package org.argeo.cms.ui.eclipse.forms.editor;
+import org.argeo.cms.ui.eclipse.forms.IManagedForm;
+import org.eclipse.swt.widgets.Control;
+/**
+ * Interface that all GUI pages need to implement in order
+ * to be added to FormEditor part. The interface makes 
+ * several assumptions:
+ * <ul>
+ * <li>The form page has a managed form</li>
+ * <li>The form page has a unique id</li>
+ * <li>The form page can be GUI but can also wrap a complete
+ * editor class (in that case, it should return <code>true</code>
+ * from <code>isEditor()</code> method).</li>
+ * <li>The form page is lazy i.e. understands that 
+ * its part control will be created at the last possible
+ * moment.</li>.
+ * </ul>
+ * <p>Existing editors can be wrapped by implementing
+ * this interface. In this case, 'isEditor' should return <code>true</code>.
+ * A common editor to wrap in <code>TextEditor</code> that is
+ * often added to show the raw source code of the file open into
+ * the multi-page editor.
+ * 
+ * @since 1.0
+ */
+public interface IFormPage {
+       /**
+        * @param editor
+        *            the form editor that this page belongs to
+        */
+       void initialize(FormEditor editor);
+       /**
+        * Returns the editor this page belongs to.
+        * 
+        * @return the form editor
+        */
+       FormEditor getEditor();
+       /**
+        * Returns the managed form of this page, unless this is a source page.
+        * 
+        * @return the managed form or <samp>null </samp> if this is a source page.
+        */
+       IManagedForm getManagedForm();
+       /**
+        * Indicates whether the page has become the active in the editor. Classes
+        * that implement this interface may use this method to commit the page (on
+        * <code>false</code>) or lazily create and/or populate the content on
+        * <code>true</code>.
+        * 
+        * @param active
+        *            <code>true</code> if page should be visible, <code>false</code>
+        *            otherwise.
+        */
+       void setActive(boolean active);
+       /**
+        * Returns <samp>true </samp> if page is currently active, false if not.
+        * 
+        * @return <samp>true </samp> for active page.
+        */
+       boolean isActive();
+       /**
+        * Tests if the content of the page is in a state that allows the
+        * editor to flip to another page. Typically, pages that contain
+        * raw source with syntax errors should not allow editors to 
+        * leave them until errors are corrected.
+        * @return <code>true</code> if the editor can flip to another page,
+        * <code>false</code> otherwise.
+        */
+       boolean canLeaveThePage();
+       /**
+        * Returns the control associated with this page.
+        * 
+        * @return the control of this page if created or <samp>null </samp> if the
+        *         page has not been shown yet.
+        */
+       Control getPartControl();
+       /**
+        * Page must have a unique id that can be used to show it without knowing
+        * its relative position in the editor.
+        * 
+        * @return the unique page identifier
+        */
+       String getId();
+       /**
+        * Returns the position of the page in the editor.
+        * 
+        * @return the zero-based index of the page in the editor.
+        */
+       int getIndex();
+       /**
+        * Sets the position of the page in the editor.
+        * 
+        * @param index
+        *            the zero-based index of the page in the editor.
+        */
+       void setIndex(int index);
+       /**
+        * Tests whether this page wraps a complete editor that
+        * can be registered on its own, or represents a page
+        * that cannot exist outside the multi-page editor context.
+        * 
+        * @return <samp>true </samp> if the page wraps an editor,
+        *         <samp>false </samp> if this is a form page.
+        */
+       boolean isEditor();
+       /**
+        * A hint to bring the provided object into focus. If the object is in a
+        * tree or table control, select it. If it is shown on a scrollable page,
+        * ensure that it is visible. If the object is not presented in 
+        * the page, <code>false</code> should be returned to allow another
+        * page to try.
+        * 
+        * @param object
+        *            object to select and reveal
+        * @return <code>true</code> if the request was successful, <code>false</code>
+        *         otherwise.
+        */
+       boolean selectReveal(Object object);
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/fs/CmsFsBrowser.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/fs/CmsFsBrowser.java
new file mode 100644 (file)
index 0000000..8aab1ec
--- /dev/null
@@ -0,0 +1,524 @@
+package org.argeo.cms.ui.fs;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.spi.FileSystemProvider;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.Session;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.fs.FileIconNameLabelProvider;
+import org.argeo.eclipse.ui.fs.FsTableViewer;
+import org.argeo.eclipse.ui.fs.FsUiConstants;
+import org.argeo.eclipse.ui.fs.FsUiUtils;
+import org.argeo.eclipse.ui.fs.NioFileLabelProvider;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.NodeUtils;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowData;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Default CMS browser composite: a sashForm layout with bookmarks at the left
+ * hand side, a simple table in the middle and an overview at right hand side.
+ */
+public class CmsFsBrowser extends Composite {
+       // private final static Log log = LogFactory.getLog(CmsFsBrowser.class);
+       private static final long serialVersionUID = -40347919096946585L;
+
+       private final FileSystemProvider nodeFileSystemProvider;
+       private final Node currentBaseContext;
+
+       // UI Parts for the browser
+       private Composite leftPannelCmp;
+       private Composite filterCmp;
+       private Text filterTxt;
+       private FsTableViewer directoryDisplayViewer;
+       private Composite rightPannelCmp;
+
+       private FsContextMenu contextMenu;
+
+       // Local context (this composite is state full)
+       private Path initialPath;
+       private Path currDisplayedFolder;
+       private Path currSelected;
+
+       // local variables (to be cleaned)
+       private int bookmarkColWith = 500;
+
+       /*
+        * WARNING: unfinalised implementation of the mechanism to retrieve base
+        * paths
+        */
+
+       private final static String NODE_PREFIX = "node://";
+
+       private String getCurrentHomePath() {
+               Session session = null;
+               try {
+                       Repository repo = currentBaseContext.getSession().getRepository();
+                       session = CurrentUser.tryAs(() -> repo.login());
+                       String homepath = NodeUtils.getUserHome(session).getPath();
+                       return homepath;
+               } catch (Exception e) {
+                       throw new CmsException("Cannot retrieve Current User Home Path", e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       protected Path[] getMyFilesPath() {
+               // return Paths.get(System.getProperty("user.dir"));
+               String currHomeUriStr = NODE_PREFIX + getCurrentHomePath();
+               try {
+                       URI uri = new URI(currHomeUriStr);
+                       FileSystem fileSystem = nodeFileSystemProvider.getFileSystem(uri);
+                       if (fileSystem == null) {
+                               PrivilegedExceptionAction<FileSystem> pea = new PrivilegedExceptionAction<FileSystem>() {
+                                       @Override
+                                       public FileSystem run() throws Exception {
+                                               return nodeFileSystemProvider.newFileSystem(uri, null);
+                                       }
+
+                               };
+                               fileSystem = CurrentUser.tryAs(pea);
+                       }
+                       Path[] paths = { fileSystem.getPath(getCurrentHomePath()), fileSystem.getPath("/") };
+                       return paths;
+               } catch (URISyntaxException | PrivilegedActionException e) {
+                       throw new RuntimeException("unable to initialise home file system for " + currHomeUriStr, e);
+               }
+       }
+
+       private Path[] getMyGroupsFilesPath() {
+               // TODO
+               Path[] paths = { Paths.get(System.getProperty("user.dir")), Paths.get("/tmp") };
+               return paths;
+       }
+
+       private Path[] getMyBookmarks() {
+               // TODO
+               Path[] paths = { Paths.get(System.getProperty("user.dir")), Paths.get("/tmp"), Paths.get("/opt") };
+               return paths;
+       }
+
+       /* End of warning */
+
+       public CmsFsBrowser(Composite parent, int style, Node context, FileSystemProvider fileSystemProvider) {
+               super(parent, style);
+               this.nodeFileSystemProvider = fileSystemProvider;
+               this.currentBaseContext = context;
+
+               this.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+               SashForm form = new SashForm(this, SWT.HORIZONTAL);
+
+               leftPannelCmp = new Composite(form, SWT.NO_FOCUS);
+               // Bookmarks are still static
+               populateBookmarks(leftPannelCmp);
+
+               Composite centerCmp = new Composite(form, SWT.BORDER | SWT.NO_FOCUS);
+               createDisplay(centerCmp);
+
+               rightPannelCmp = new Composite(form, SWT.NO_FOCUS);
+
+               form.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               form.setWeights(new int[] { 15, 40, 20 });
+       }
+
+       void refresh() {
+               modifyFilter(false);
+               // also refresh bookmarks and groups
+       }
+
+       private void createDisplay(final Composite parent) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+               // top filter
+               filterCmp = new Composite(parent, SWT.NO_FOCUS);
+               filterCmp.setLayoutData(EclipseUiUtils.fillWidth());
+               addFilterPanel(filterCmp);
+
+               // Main display
+               directoryDisplayViewer = new FsTableViewer(parent, SWT.MULTI);
+               List<ColumnDefinition> colDefs = new ArrayList<>();
+               colDefs.add(new ColumnDefinition(new FileIconNameLabelProvider(), "Name", 250));
+               colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_SIZE), "Size", 100));
+               colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_TYPE), "Type", 150));
+               colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_LAST_MODIFIED),
+                               "Last modified", 400));
+               final Table table = directoryDisplayViewer.configureDefaultTable(colDefs);
+               table.setLayoutData(EclipseUiUtils.fillAll());
+
+               // table.addKeyListener(new KeyListener() {
+               // private static final long serialVersionUID = -8083424284436715709L;
+               //
+               // @Override
+               // public void keyReleased(KeyEvent e) {
+               // }
+               //
+               // @Override
+               // public void keyPressed(KeyEvent e) {
+               // if (log.isDebugEnabled())
+               // log.debug("Key event received: " + e.keyCode);
+               // IStructuredSelection selection = (IStructuredSelection)
+               // directoryDisplayViewer.getSelection();
+               // Path selected = null;
+               // if (!selection.isEmpty())
+               // selected = ((Path) selection.getFirstElement());
+               // if (e.keyCode == SWT.CR) {
+               // if (!Files.isDirectory(selected))
+               // return;
+               // if (selected != null) {
+               // currDisplayedFolder = selected;
+               // directoryDisplayViewer.setInput(currDisplayedFolder, "*");
+               // }
+               // } else if (e.keyCode == SWT.BS) {
+               // currDisplayedFolder = currDisplayedFolder.getParent();
+               // directoryDisplayViewer.setInput(currDisplayedFolder, "*");
+               // directoryDisplayViewer.getTable().setFocus();
+               // }
+               // }
+               // });
+
+               directoryDisplayViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+
+                       @Override
+                       public void selectionChanged(SelectionChangedEvent event) {
+                               IStructuredSelection selection = (IStructuredSelection) directoryDisplayViewer.getSelection();
+                               Path selected = null;
+                               if (selection.isEmpty())
+                                       setSelected(null);
+                               else
+                                       selected = ((Path) selection.getFirstElement());
+                               if (selected != null) {
+                                       // TODO manage multiple selection
+                                       setSelected(selected);
+                               }
+                       }
+               });
+
+               directoryDisplayViewer.addDoubleClickListener(new IDoubleClickListener() {
+                       @Override
+                       public void doubleClick(DoubleClickEvent event) {
+                               IStructuredSelection selection = (IStructuredSelection) directoryDisplayViewer.getSelection();
+                               Path selected = null;
+                               if (!selection.isEmpty())
+                                       selected = ((Path) selection.getFirstElement());
+                               if (selected != null) {
+                                       if (!Files.isDirectory(selected))
+                                               return;
+                                       setInput(selected);
+                               }
+                       }
+               });
+
+               // The context menu
+               contextMenu = new FsContextMenu(this);
+
+               table.addMouseListener(new MouseAdapter() {
+                       private static final long serialVersionUID = 6737579410648595940L;
+
+                       @Override
+                       public void mouseDown(MouseEvent e) {
+                               if (e.button == 3) {
+                                       // contextMenu.setCurrFolderPath(currDisplayedFolder);
+                                       contextMenu.show(table, new Point(e.x, e.y), currDisplayedFolder);
+                               }
+                       }
+               });
+       }
+
+       private void addPathElementBtn(Path path) {
+               Button elemBtn = new Button(filterCmp, SWT.PUSH);
+               String nameStr;
+               if (path.toString().equals("/"))
+                       nameStr = "[jcr:root]";
+               else
+                       nameStr = path.getFileName().toString();
+               elemBtn.setText(nameStr + " >> ");
+               CmsUtils.style(elemBtn, FsStyles.BREAD_CRUMB_BTN);
+               elemBtn.addSelectionListener(new SelectionAdapter() {
+                       private static final long serialVersionUID = -4103695476023480651L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               setInput(path);
+                       }
+               });
+       }
+
+       public void setInput(Path path) {
+               if (path.equals(currDisplayedFolder))
+                       return;
+               currDisplayedFolder = path;
+
+               Path diff = initialPath.relativize(currDisplayedFolder);
+
+               for (Control child : filterCmp.getChildren())
+                       if (!child.equals(filterTxt))
+                               child.dispose();
+
+               addPathElementBtn(initialPath);
+               Path currTarget = initialPath;
+               if (!diff.toString().equals(""))
+                       for (Path pathElem : diff) {
+                               currTarget = currTarget.resolve(pathElem);
+                               addPathElementBtn(currTarget);
+                       }
+
+               filterTxt.setText("");
+               filterTxt.moveBelow(null);
+               setSelected(null);
+               filterCmp.getParent().layout(true, true);
+       }
+
+       private void setSelected(Path path) {
+               currSelected = path;
+               setOverviewInput(path);
+       }
+
+       public Viewer getViewer() {
+               return directoryDisplayViewer;
+       }
+
+       private void populateBookmarks(Composite parent) {
+               CmsUtils.clear(parent);
+               parent.setLayout(new GridLayout());
+               ISelectionChangedListener selList = new BookmarksSelChangeListener();
+
+               FsTableViewer homeViewer = new FsTableViewer(parent, SWT.SINGLE | SWT.NO_SCROLL);
+               Table table = homeViewer.configureDefaultSingleColumnTable(bookmarkColWith);
+               GridData gd = EclipseUiUtils.fillWidth();
+               gd.horizontalIndent = 10;
+               table.setLayoutData(gd);
+               homeViewer.addSelectionChangedListener(selList);
+               homeViewer.setPathsInput(getMyFilesPath());
+
+               appendTitle(parent, "Shared files");
+               FsTableViewer groupsViewer = new FsTableViewer(parent, SWT.SINGLE | SWT.NO_SCROLL);
+               table = groupsViewer.configureDefaultSingleColumnTable(bookmarkColWith);
+               gd = EclipseUiUtils.fillWidth();
+               gd.horizontalIndent = 10;
+               table.setLayoutData(gd);
+               groupsViewer.addSelectionChangedListener(selList);
+               groupsViewer.setPathsInput(getMyGroupsFilesPath());
+
+               appendTitle(parent, "My bookmarks");
+               FsTableViewer bookmarksViewer = new FsTableViewer(parent, SWT.SINGLE | SWT.NO_SCROLL);
+               table = bookmarksViewer.configureDefaultSingleColumnTable(bookmarkColWith);
+               gd = EclipseUiUtils.fillWidth();
+               gd.horizontalIndent = 10;
+               table.setLayoutData(gd);
+               bookmarksViewer.addSelectionChangedListener(selList);
+               bookmarksViewer.setPathsInput(getMyBookmarks());
+       }
+
+       /**
+        * Recreates the content of the box that displays information about the
+        * current selected Path.
+        */
+       private void setOverviewInput(Path path) {
+               try {
+                       EclipseUiUtils.clear(rightPannelCmp);
+                       rightPannelCmp.setLayout(new GridLayout());
+                       if (path != null) {
+                               // if (isImg(context)) {
+                               // EditableImage image = new Img(parent, RIGHT, context,
+                               // imageWidth);
+                               // image.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER,
+                               // true, false,
+                               // 2, 1));
+                               // }
+
+                               Label contextL = new Label(rightPannelCmp, SWT.NONE);
+                               contextL.setText(path.getFileName().toString());
+                               contextL.setFont(EclipseUiUtils.getBoldFont(rightPannelCmp));
+                               addProperty(rightPannelCmp, "Last modified", Files.getLastModifiedTime(path).toString());
+                               // addProperty(rightPannelCmp, "Owner",
+                               // Files.getOwner(path).getName());
+                               if (Files.isDirectory(path)) {
+                                       addProperty(rightPannelCmp, "Type", "Folder");
+                               } else {
+                                       String mimeType = Files.probeContentType(path);
+                                       if (EclipseUiUtils.isEmpty(mimeType))
+                                               mimeType = "<i>Unknown</i>";
+                                       addProperty(rightPannelCmp, "Type", mimeType);
+                                       addProperty(rightPannelCmp, "Size", FsUiUtils.humanReadableByteCount(Files.size(path), false));
+                               }
+                       }
+                       rightPannelCmp.layout(true, true);
+               } catch (IOException e) {
+                       throw new CmsException("Cannot display details for " + path.toString(), e);
+               }
+       }
+
+       private void addFilterPanel(Composite parent) {
+               RowLayout rl = new RowLayout(SWT.HORIZONTAL);
+               rl.wrap = true;
+               parent.setLayout(rl);
+               // parent.setLayout(EclipseUiUtils.noSpaceGridLayout(new GridLayout(2,
+               // false)));
+
+               filterTxt = new Text(parent, SWT.SEARCH | SWT.ICON_CANCEL);
+               filterTxt.setMessage("Search current folder");
+               filterTxt.setLayoutData(new RowData(250, SWT.DEFAULT));
+               filterTxt.addModifyListener(new ModifyListener() {
+                       private static final long serialVersionUID = 1L;
+
+                       public void modifyText(ModifyEvent event) {
+                               modifyFilter(false);
+                       }
+               });
+               filterTxt.addKeyListener(new KeyListener() {
+                       private static final long serialVersionUID = 2533535233583035527L;
+
+                       @Override
+                       public void keyReleased(KeyEvent e) {
+                       }
+
+                       @Override
+                       public void keyPressed(KeyEvent e) {
+                               // boolean shiftPressed = (e.stateMask & SWT.SHIFT) != 0;
+                               // // boolean altPressed = (e.stateMask & SWT.ALT) != 0;
+                               // FilterEntitiesVirtualTable currTable = null;
+                               // if (currEdited != null) {
+                               // FilterEntitiesVirtualTable table =
+                               // browserCols.get(currEdited);
+                               // if (table != null && !table.isDisposed())
+                               // currTable = table;
+                               // }
+                               //
+                               // if (e.keyCode == SWT.ARROW_DOWN)
+                               // currTable.setFocus();
+                               // else if (e.keyCode == SWT.BS) {
+                               // if (filterTxt.getText().equals("")
+                               // && !(currEdited.getNameCount() == 1 ||
+                               // currEdited.equals(initialPath))) {
+                               // Path oldEdited = currEdited;
+                               // Path parentPath = currEdited.getParent();
+                               // setEdited(parentPath);
+                               // if (browserCols.containsKey(parentPath))
+                               // browserCols.get(parentPath).setSelected(oldEdited);
+                               // filterTxt.setFocus();
+                               // e.doit = false;
+                               // }
+                               // } else if (e.keyCode == SWT.TAB && !shiftPressed) {
+                               // Path uniqueChild = getOnlyChild(currEdited,
+                               // filterTxt.getText());
+                               // if (uniqueChild != null) {
+                               // // Highlight the unique chosen child
+                               // currTable.setSelected(uniqueChild);
+                               // setEdited(uniqueChild);
+                               // }
+                               // filterTxt.setFocus();
+                               // e.doit = false;
+                               // }
+                       }
+               });
+       }
+
+       private Path getOnlyChild(Path parent, String filter) {
+               try (DirectoryStream<Path> stream = Files.newDirectoryStream(currDisplayedFolder, filter + "*")) {
+                       Path uniqueChild = null;
+                       boolean moreThanOne = false;
+                       loop: for (Path entry : stream) {
+                               if (uniqueChild == null) {
+                                       uniqueChild = entry;
+                               } else {
+                                       moreThanOne = true;
+                                       break loop;
+                               }
+                       }
+                       if (!moreThanOne)
+                               return uniqueChild;
+                       return null;
+               } catch (IOException ioe) {
+                       throw new CmsException(
+                                       "Unable to determine unique child existence and get it under " + parent + " with filter " + filter,
+                                       ioe);
+               }
+       }
+
+       private void modifyFilter(boolean fromOutside) {
+               if (!fromOutside)
+                       if (currDisplayedFolder != null) {
+                               String filter = filterTxt.getText() + "*";
+                               directoryDisplayViewer.setInput(currDisplayedFolder, filter);
+                       }
+       }
+
+       private class BookmarksSelChangeListener implements ISelectionChangedListener {
+
+               @Override
+               public void selectionChanged(SelectionChangedEvent event) {
+                       IStructuredSelection selection = (IStructuredSelection) event.getSelection();
+                       if (selection.isEmpty())
+                               return;
+                       else {
+                               Path newSelected = (Path) selection.getFirstElement();
+                               if (newSelected.equals(currDisplayedFolder) && newSelected.equals(initialPath))
+                                       return;
+                               initialPath = newSelected;
+                               setInput(newSelected);
+                       }
+               }
+       }
+
+       // Simplify UI implementation
+       private void addProperty(Composite parent, String propName, String value) {
+               Label contextL = new Label(parent, SWT.NONE);
+               contextL.setText(propName + ": " + value);
+       }
+
+       private Label appendTitle(Composite parent, String value) {
+               Label titleLbl = new Label(parent, SWT.NONE);
+               titleLbl.setText(value);
+               titleLbl.setFont(EclipseUiUtils.getBoldFont(parent));
+               GridData gd = EclipseUiUtils.fillWidth();
+               gd.horizontalIndent = 5;
+               gd.verticalIndent = 5;
+               titleLbl.setLayoutData(gd);
+               return titleLbl;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/fs/FsContextMenu.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/fs/FsContextMenu.java
new file mode 100644 (file)
index 0000000..ffb5f0a
--- /dev/null
@@ -0,0 +1,384 @@
+package org.argeo.cms.ui.fs;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.dialogs.SingleValue;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/** Generic popup context menu to manage NIO Path in a Viewer. */
+public class FsContextMenu extends Shell {
+       private static final long serialVersionUID = -9120261153509855795L;
+
+       private final static Log log = LogFactory.getLog(FsContextMenu.class);
+
+       // Default known actions
+       public final static String ACTION_ID_CREATE_FOLDER = "createFolder";
+       public final static String ACTION_ID_BOOKMARK_FOLDER = "bookmarkFolder";
+       public final static String ACTION_ID_SHARE_FOLDER = "shareFolder";
+       public final static String ACTION_ID_DOWNLOAD_FOLDER = "downloadFolder";
+       public final static String ACTION_ID_DELETE = "delete";
+       public final static String ACTION_ID_UPLOAD_FILE = "uploadFiles";
+       public final static String ACTION_ID_OPEN = "open";
+
+       // Local context
+       private final CmsFsBrowser browser;
+       // private final Viewer viewer;
+       private final static String KEY_ACTION_ID = "actionId";
+       private final static String[] DEFAULT_ACTIONS = { ACTION_ID_CREATE_FOLDER, ACTION_ID_BOOKMARK_FOLDER,
+                       ACTION_ID_SHARE_FOLDER, ACTION_ID_DOWNLOAD_FOLDER, ACTION_ID_DELETE, ACTION_ID_UPLOAD_FILE,
+                       ACTION_ID_OPEN };
+       private Map<String, Button> actionButtons = new HashMap<String, Button>();
+
+       private Path currFolderPath;
+
+       public FsContextMenu(CmsFsBrowser browser) { // Viewer viewer, Display
+                                                                                                       // display) {
+               super(browser.getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+               this.browser = browser;
+               setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+               Composite boxCmp = new Composite(this, SWT.NO_FOCUS | SWT.BORDER);
+               boxCmp.setLayout(EclipseUiUtils.noSpaceGridLayout());
+               CmsUtils.style(boxCmp, FsStyles.CONTEXT_MENU_BOX);
+               createContextMenu(boxCmp);
+
+               addShellListener(new ActionsShellListener());
+       }
+
+       protected void createContextMenu(Composite boxCmp) {
+               ActionsSelListener asl = new ActionsSelListener();
+               for (String actionId : DEFAULT_ACTIONS) {
+                       Button btn = new Button(boxCmp, SWT.FLAT | SWT.PUSH | SWT.LEAD);
+                       btn.setText(getLabel(actionId));
+                       btn.setLayoutData(EclipseUiUtils.fillWidth());
+                       CmsUtils.markup(btn);
+                       CmsUtils.style(btn, actionId + FsStyles.BUTTON_SUFFIX);
+                       btn.setData(KEY_ACTION_ID, actionId);
+                       btn.addSelectionListener(asl);
+                       actionButtons.put(actionId, btn);
+               }
+       }
+
+       protected String getLabel(String actionId) {
+               switch (actionId) {
+               case ACTION_ID_CREATE_FOLDER:
+                       return "Create Folder";
+               case ACTION_ID_BOOKMARK_FOLDER:
+                       return "Bookmark Folder";
+               case ACTION_ID_SHARE_FOLDER:
+                       return "Share Folder";
+               case ACTION_ID_DOWNLOAD_FOLDER:
+                       return "Download as zip archive";
+               case ACTION_ID_DELETE:
+                       return "Delete";
+               case ACTION_ID_UPLOAD_FILE:
+                       return "Upload Files";
+               case ACTION_ID_OPEN:
+                       return "Open";
+               default:
+                       throw new IllegalArgumentException("Unknown action ID " + actionId);
+               }
+       }
+
+       protected void aboutToShow(Control source, Point location) {
+               IStructuredSelection selection = ((IStructuredSelection) browser.getViewer().getSelection());
+               boolean emptySel = true;
+               boolean multiSel = false;
+               boolean isFolder = true;
+               if (selection != null && !selection.isEmpty()) {
+                       emptySel = false;
+                       multiSel = selection.size() > 1;
+                       if (!multiSel && selection.getFirstElement() instanceof Path) {
+                               isFolder = Files.isDirectory((Path) selection.getFirstElement());
+                       }
+               }
+               if (emptySel) {
+                       setVisible(true, ACTION_ID_CREATE_FOLDER, ACTION_ID_UPLOAD_FILE);
+                       setVisible(false, ACTION_ID_SHARE_FOLDER, ACTION_ID_DOWNLOAD_FOLDER, ACTION_ID_DELETE, ACTION_ID_OPEN,
+                                       // to be implemented
+                                       ACTION_ID_BOOKMARK_FOLDER);
+               } else if (multiSel) {
+                       setVisible(true, ACTION_ID_CREATE_FOLDER, ACTION_ID_UPLOAD_FILE, ACTION_ID_DELETE);
+                       setVisible(false, ACTION_ID_SHARE_FOLDER, ACTION_ID_DOWNLOAD_FOLDER, ACTION_ID_OPEN,
+                                       // to be implemented
+                                       ACTION_ID_BOOKMARK_FOLDER);
+               } else if (isFolder) {
+                       setVisible(true, ACTION_ID_CREATE_FOLDER, ACTION_ID_UPLOAD_FILE, ACTION_ID_DELETE);
+                       setVisible(false, ACTION_ID_OPEN,
+                                       // to be implemented
+                                       ACTION_ID_SHARE_FOLDER, ACTION_ID_DOWNLOAD_FOLDER, ACTION_ID_BOOKMARK_FOLDER);
+               } else {
+                       setVisible(true, ACTION_ID_CREATE_FOLDER, ACTION_ID_UPLOAD_FILE, ACTION_ID_OPEN, ACTION_ID_DELETE);
+                       setVisible(false, ACTION_ID_SHARE_FOLDER, ACTION_ID_DOWNLOAD_FOLDER,
+                                       // to be implemented
+                                       ACTION_ID_BOOKMARK_FOLDER);
+               }
+       }
+
+       private void setVisible(boolean visible, String... buttonIds) {
+               for (String id : buttonIds) {
+                       Button button = actionButtons.get(id);
+                       button.setVisible(visible);
+                       GridData gd = (GridData) button.getLayoutData();
+                       gd.heightHint = visible ? SWT.DEFAULT : 0;
+               }
+       }
+
+       public void show(Control source, Point location, Path currFolderPath) {
+               if (isVisible())
+                       setVisible(false);
+               // TODO find a better way to retrieve the parent path (cannot be deduced
+               // from table content because it will fail on an empty folder)
+               this.currFolderPath = currFolderPath;
+               aboutToShow(source, location);
+               pack();
+               layout();
+               if (source instanceof Control)
+                       setLocation(((Control) source).toDisplay(location.x, location.y));
+               open();
+       }
+
+       class StyleButton extends Label {
+               private static final long serialVersionUID = 7731102609123946115L;
+
+               public StyleButton(Composite parent, int swtStyle) {
+                       super(parent, swtStyle);
+               }
+
+       }
+
+       // class ActionsMouseListener extends MouseAdapter {
+       // private static final long serialVersionUID = -1041871937815812149L;
+       //
+       // @Override
+       // public void mouseDown(MouseEvent e) {
+       // Object eventSource = e.getSource();
+       // if (e.button == 1) {
+       // if (eventSource instanceof Button) {
+       // Button pressedBtn = (Button) eventSource;
+       // String actionId = (String) pressedBtn.getData(KEY_ACTION_ID);
+       // switch (actionId) {
+       // case ACTION_ID_CREATE_FOLDER:
+       // createFolder();
+       // break;
+       // case ACTION_ID_DELETE:
+       // deleteItems();
+       // break;
+       // default:
+       // throw new IllegalArgumentException("Unimplemented action " + actionId);
+       // // case ACTION_ID_SHARE_FOLDER:
+       // // return "Share Folder";
+       // // case ACTION_ID_DOWNLOAD_FOLDER:
+       // // return "Download as zip archive";
+       // // case ACTION_ID_UPLOAD_FILE:
+       // // return "Upload Files";
+       // // case ACTION_ID_OPEN:
+       // // return "Open";
+       // }
+       // }
+       // }
+       // viewer.getControl().setFocus();
+       // // setVisible(false);
+       // }
+       // }
+
+       class ActionsSelListener extends SelectionAdapter {
+               private static final long serialVersionUID = -1041871937815812149L;
+
+               @Override
+               public void widgetSelected(SelectionEvent e) {
+                       Object eventSource = e.getSource();
+                       if (eventSource instanceof Button) {
+                               Button pressedBtn = (Button) eventSource;
+                               String actionId = (String) pressedBtn.getData(KEY_ACTION_ID);
+                               switch (actionId) {
+                               case ACTION_ID_CREATE_FOLDER:
+                                       createFolder();
+                                       break;
+                               case ACTION_ID_DELETE:
+                                       deleteItems();
+                                       break;
+                               case ACTION_ID_OPEN:
+                                       openFile();
+                                       break;
+                               case ACTION_ID_UPLOAD_FILE:
+                                       uploadFiles();
+                                       break;
+                               default:
+                                       throw new IllegalArgumentException("Unimplemented action " + actionId);
+                                       // case ACTION_ID_SHARE_FOLDER:
+                                       // return "Share Folder";
+                                       // case ACTION_ID_DOWNLOAD_FOLDER:
+                                       // return "Download as zip archive";
+                                       // case ACTION_ID_OPEN:
+                                       // return "Open";
+                               }
+                       }
+                       browser.setFocus();
+                       // viewer.getControl().setFocus();
+                       // setVisible(false);
+
+               }
+       }
+
+       class ActionsShellListener extends org.eclipse.swt.events.ShellAdapter {
+               private static final long serialVersionUID = -5092341449523150827L;
+
+               @Override
+               public void shellDeactivated(ShellEvent e) {
+                       setVisible(false);
+               }
+       }
+
+       private void openFile() {
+               log.warn("Implement single sourced, workbench independant \"Open File\" action");
+       }
+
+       private void deleteItems() {
+               IStructuredSelection selection = ((IStructuredSelection) browser.getViewer().getSelection());
+               if (selection.isEmpty())
+                       return;
+
+               StringBuilder builder = new StringBuilder();
+               @SuppressWarnings("unchecked")
+               Iterator<Object> iterator = selection.iterator();
+               List<Path> paths = new ArrayList<>();
+
+               while (iterator.hasNext()) {
+                       Path path = (Path) iterator.next();
+                       builder.append(path.getFileName() + ", ");
+                       paths.add(path);
+               }
+               String msg = "You are about to delete following elements: " + builder.substring(0, builder.length() - 2)
+                               + ". Are you sure?";
+               if (MessageDialog.openConfirm(this, "Confirm deletion", msg)) {
+                       for (Path path : paths) {
+                               try {
+                                       // Might have already been deleted if we are in a tree
+                                       Files.deleteIfExists(path);
+                               } catch (IOException e) {
+                                       throw new CmsException("Cannot delete path " + path, e);
+                               }
+                       }
+                       browser.refresh();
+               }
+       }
+
+       private void createFolder() {
+               String msg = "Please provide a name.";
+               String name = SingleValue.ask("Create folder", msg);
+               // TODO enhance check of name validity
+               if (EclipseUiUtils.notEmpty(name)) {
+                       try {
+                               Path child = currFolderPath.resolve(name);
+                               if (Files.exists(child))
+                                       throw new CmsException("An item with name " + name + " already exists at "
+                                                       + currFolderPath.toString() + ", cannot create");
+                               else
+                                       Files.createDirectories(child);
+                               browser.refresh();
+                       } catch (IOException e) {
+                               throw new CmsException("Cannot create folder " + name + " at " + currFolderPath.toString(), e);
+                       }
+               }
+       }
+
+       private void uploadFiles() {
+               try {
+                       FileDialog dialog = new FileDialog(browser.getShell(), SWT.MULTI);
+                       dialog.setText("Choose one or more files to upload");
+
+                       if (EclipseUiUtils.notEmpty(dialog.open())) {
+                               String[] names = dialog.getFileNames();
+                               // Workaround small differences between RAP and RCP
+                               // 1. returned names are absolute path on RAP and
+                               // relative in RCP
+                               // 2. in RCP we must use getFilterPath that does not
+                               // exists on RAP
+                               Method filterMethod = null;
+                               Path parPath = null;
+                               try {
+                                       filterMethod = dialog.getClass().getDeclaredMethod("getFilterPath");
+                                       String filterPath = (String) filterMethod.invoke(dialog);
+                                       parPath = Paths.get(filterPath);
+                               } catch (NoSuchMethodException nsme) { // RAP
+                               }
+                               if (names.length == 0)
+                                       return;
+                               else {
+                                       loop: for (String name : names) {
+                                               Path tmpPath = Paths.get(name);
+                                               if (parPath != null)
+                                                       tmpPath = parPath.resolve(tmpPath);
+                                               if (Files.exists(tmpPath)) {
+                                                       URI uri = tmpPath.toUri();
+                                                       String uriStr = uri.toString();
+
+                                                       if (Files.isDirectory(tmpPath)) {
+                                                               MessageDialog.openError(browser.getShell(), "Unimplemented directory import",
+                                                                               "Upload of directories in the system is not yet implemented");
+                                                               continue loop;
+                                                       }
+                                                       Path targetPath = currFolderPath.resolve(tmpPath.getFileName().toString());
+                                                       InputStream in = null;
+                                                       try {
+                                                               in = new ByteArrayInputStream(Files.readAllBytes(tmpPath));
+                                                               Files.copy(in, targetPath);
+                                                               Files.delete(tmpPath);
+                                                       } finally {
+                                                               IOUtils.closeQuietly(in);
+                                                       }
+                                                       if (log.isDebugEnabled())
+                                                               log.debug("copied uploaded file " + uriStr + " to " + targetPath.toString());
+                                               } else {
+                                                       String msg = "Cannot copy tmp file from " + tmpPath.toString();
+                                                       if (parPath != null)
+                                                               msg += "\nPlease remember that file upload fails when choosing files from the \"Recently Used\" bookmarks on some OS";
+                                                       MessageDialog.openError(browser.getShell(), "Missing file", msg);
+                                                       continue loop;
+                                               }
+                                       }
+                                       browser.refresh();
+                               }
+                       }
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       MessageDialog.openError(getShell(), "Upload has failed", "Cannot import files to " + currFolderPath);
+               }
+       }
+
+       public void setCurrFolderPath(Path currFolderPath) {
+               this.currFolderPath = currFolderPath;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/fs/FsStyles.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/fs/FsStyles.java
new file mode 100644 (file)
index 0000000..9ae3192
--- /dev/null
@@ -0,0 +1,8 @@
+package org.argeo.cms.ui.fs;
+
+/** FS Ui specific CSS styles */
+public interface FsStyles {
+       String BREAD_CRUMB_BTN = "breadCrumb_btn";
+       String CONTEXT_MENU_BOX = "contextMenu_box";
+       String BUTTON_SUFFIX = "_btn";
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/Activator.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/Activator.java
new file mode 100644 (file)
index 0000000..6417b25
--- /dev/null
@@ -0,0 +1,37 @@
+package org.argeo.cms.ui.internal;
+
+import org.argeo.node.NodeState;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class Activator implements BundleActivator {
+
+       // avoid dependency to RWT OSGi
+       private final static String CONTEXT_NAME_PROP = "contextName";
+
+       private static ServiceTracker<NodeState, NodeState> nodeState;
+
+       // @Override
+       public void start(BundleContext bc) throws Exception {
+               // UI
+//             bc.registerService(ApplicationConfiguration.class, new MaintenanceUi(),
+//                             LangUtils.dico(CONTEXT_NAME_PROP, "system"));
+//             bc.registerService(ApplicationConfiguration.class, new UserUi(), LangUtils.dico(CONTEXT_NAME_PROP, "user"));
+
+               nodeState = new ServiceTracker<>(bc, NodeState.class, null);
+               nodeState.open();
+       }
+
+       @Override
+       public void stop(BundleContext context) throws Exception {
+               if (nodeState != null) {
+                       nodeState.close();
+                       nodeState = null;
+               }
+       }
+
+       public static NodeState getNodeState() {
+               return nodeState.getService();
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/ImageManagerImpl.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/ImageManagerImpl.java
new file mode 100644 (file)
index 0000000..8554462
--- /dev/null
@@ -0,0 +1,263 @@
+package org.argeo.cms.ui.internal;
+
+import static javax.jcr.Node.JCR_CONTENT;
+import static javax.jcr.Property.JCR_DATA;
+import static javax.jcr.nodetype.NodeType.NT_FILE;
+import static javax.jcr.nodetype.NodeType.NT_RESOURCE;
+import static org.argeo.cms.CmsTypes.CMS_STYLED;
+import static org.argeo.cms.ui.CmsConstants.NO_IMAGE_SIZE;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.activation.MimetypesFileTypeMap;
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.CmsTypes;
+import org.argeo.cms.ui.CmsImageManager;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.service.ResourceManager;
+import org.eclipse.rap.rwt.widgets.FileUpload;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+
+/** Manages only public images so far. */
+public class ImageManagerImpl implements CmsImageManager, CmsNames {
+       private final static Log log = LogFactory.getLog(ImageManagerImpl.class);
+       private MimetypesFileTypeMap fileTypeMap = new MimetypesFileTypeMap();
+
+       public Boolean load(Node node, Control control, Point preferredSize)
+                       throws RepositoryException {
+               Point imageSize = getImageSize(node);
+               Point size;
+               String imgTag = null;
+               if (preferredSize == null || imageSize.x == 0 || imageSize.y == 0
+                               || (preferredSize.x == 0 && preferredSize.y == 0)) {
+                       if (imageSize.x != 0 && imageSize.y != 0) {
+                               // actual image size if completely known
+                               size = imageSize;
+                       } else {
+                               // no image if not completely known
+                               size = resizeTo(NO_IMAGE_SIZE,
+                                               preferredSize != null ? preferredSize : imageSize);
+                               imgTag = CmsUtils.noImg(size);
+                       }
+
+               } else if (preferredSize.x != 0 && preferredSize.y != 0) {
+                       // given size if completely provided
+                       size = preferredSize;
+               } else {
+                       // at this stage :
+                       // image is completely known
+                       assert imageSize.x != 0 && imageSize.y != 0;
+                       // one and only one of the dimension as been specified
+                       assert preferredSize.x == 0 || preferredSize.y == 0;
+                       size = resizeTo(imageSize, preferredSize);
+               }
+
+               boolean loaded = false;
+               if (control == null)
+                       return loaded;
+
+               if (control instanceof Label) {
+                       if (imgTag == null) {
+                               // IMAGE RETRIEVED HERE
+                               imgTag = getImageTag(node, size);
+                               //
+                               if (imgTag == null)
+                                       imgTag = CmsUtils.noImg(size);
+                               else
+                                       loaded = true;
+                       }
+
+                       Label lbl = (Label) control;
+                       lbl.setText(imgTag);
+                       // lbl.setSize(size);
+               } else if (control instanceof FileUpload) {
+                       FileUpload lbl = (FileUpload) control;
+                       lbl.setImage(CmsUtils.noImage(size));
+                       lbl.setSize(size);
+                       return loaded;
+               } else
+                       loaded = false;
+
+               return loaded;
+       }
+
+       private Point resizeTo(Point orig, Point constraints) {
+               if (constraints.x != 0 && constraints.y != 0) {
+                       return constraints;
+               } else if (constraints.x == 0 && constraints.y == 0) {
+                       return orig;
+               } else if (constraints.y == 0) {// force width
+                       return new Point(constraints.x,
+                                       scale(orig.y, orig.x, constraints.x));
+               } else if (constraints.x == 0) {// force height
+                       return new Point(scale(orig.x, orig.y, constraints.y),
+                                       constraints.y);
+               }
+               throw new CmsException("Cannot resize " + orig + " to " + constraints);
+       }
+
+       private int scale(int origDimension, int otherDimension, int otherConstraint) {
+               return Math.round(origDimension
+                               * divide(otherConstraint, otherDimension));
+       }
+
+       private float divide(int a, int b) {
+               return ((float) a) / ((float) b);
+       }
+
+       public Point getImageSize(Node node) throws RepositoryException {
+               return new Point(node.hasProperty(CMS_IMAGE_WIDTH) ? (int) node
+                               .getProperty(CMS_IMAGE_WIDTH).getLong() : 0,
+                               node.hasProperty(CMS_IMAGE_WIDTH) ? (int) node.getProperty(
+                                               CMS_IMAGE_HEIGHT).getLong() : 0);
+       }
+
+       /** @return null if not available */
+       @Override
+       public String getImageTag(Node node) throws RepositoryException {
+               return getImageTag(node, getImageSize(node));
+       }
+
+       private String getImageTag(Node node, Point size)
+                       throws RepositoryException {
+               StringBuilder buf = getImageTagBuilder(node, size);
+               if (buf == null)
+                       return null;
+               return buf.append("/>").toString();
+       }
+
+       /** @return null if not available */
+       @Override
+       public StringBuilder getImageTagBuilder(Node node, Point size)
+                       throws RepositoryException {
+               return getImageTagBuilder(node, Integer.toString(size.x),
+                               Integer.toString(size.y));
+       }
+
+       /** @return null if not available */
+       private StringBuilder getImageTagBuilder(Node node, String width,
+                       String height) throws RepositoryException {
+               String url = getImageUrl(node);
+               if (url == null)
+                       return null;
+               return CmsUtils.imgBuilder(url, width, height);
+       }
+
+       /** @return null if not available */
+       @Override
+       public String getImageUrl(Node node) throws RepositoryException {
+               return CmsUtils.getDataPath(node);
+               // String name = getResourceName(node);
+               // ResourceManager resourceManager = RWT.getResourceManager();
+               // if (!resourceManager.isRegistered(name)) {
+               // InputStream inputStream = null;
+               // Binary binary = getImageBinary(node);
+               // if (binary == null)
+               // return null;
+               // try {
+               // inputStream = binary.getStream();
+               // resourceManager.register(name, inputStream);
+               // } finally {
+               // IOUtils.closeQuietly(inputStream);
+               // JcrUtils.closeQuietly(binary);
+               // }
+               // if (log.isTraceEnabled())
+               // log.trace("Registered image " + name);
+               // }
+               // return resourceManager.getLocation(name);
+       }
+
+       protected String getResourceName(Node node) throws RepositoryException {
+               String workspace = node.getSession().getWorkspace().getName();
+               if (node.hasNode(JCR_CONTENT))
+                       return workspace + '_' + node.getNode(JCR_CONTENT).getIdentifier();
+               else
+                       return workspace + '_' + node.getIdentifier();
+       }
+
+       public Binary getImageBinary(Node node) throws RepositoryException {
+               if (node.isNodeType(NT_FILE))
+                       return node.getNode(JCR_CONTENT).getProperty(JCR_DATA).getBinary();
+               else if (node.isNodeType(CMS_STYLED) && node.hasProperty(CMS_DATA)) {
+                       return node.getProperty(CMS_DATA).getBinary();
+               } else {
+                       return null;
+               }
+       }
+
+       public Image getSwtImage(Node node) throws RepositoryException {
+               InputStream inputStream = null;
+               Binary binary = getImageBinary(node);
+               if (binary == null)
+                       return null;
+               try {
+                       inputStream = binary.getStream();
+                       return new Image(Display.getCurrent(), inputStream);
+               } finally {
+                       IOUtils.closeQuietly(inputStream);
+                       JcrUtils.closeQuietly(binary);
+               }
+       }
+
+       @Override
+       public String uploadImage(Node parentNode, String fileName, InputStream in)
+                       throws RepositoryException {
+               InputStream inputStream = null;
+               try {
+                       String previousResourceName = null;
+                       if (parentNode.hasNode(fileName)) {
+                               Node node = parentNode.getNode(fileName);
+                               previousResourceName = getResourceName(node);
+                               if (node.hasNode(JCR_CONTENT)) {
+                                       node.getNode(JCR_CONTENT).remove();
+                                       node.addNode(JCR_CONTENT, NT_RESOURCE);
+                               }
+                       }
+
+                       byte[] arr = IOUtils.toByteArray(in);
+                       Node fileNode = JcrUtils.copyBytesAsFile(parentNode, fileName, arr);
+                       fileNode.addMixin(CmsTypes.CMS_IMAGE);
+
+                       inputStream = new ByteArrayInputStream(arr);
+                       ImageData id = new ImageData(inputStream);
+                       fileNode.setProperty(CMS_IMAGE_WIDTH, id.width);
+                       fileNode.setProperty(CMS_IMAGE_HEIGHT, id.height);
+                       fileNode.setProperty(Property.JCR_MIMETYPE,
+                                       fileTypeMap.getContentType(fileName));
+                       fileNode.getSession().save();
+
+                       // reset resource manager
+                       ResourceManager resourceManager = RWT.getResourceManager();
+                       if (previousResourceName != null
+                                       && resourceManager.isRegistered(previousResourceName)) {
+                               resourceManager.unregister(previousResourceName);
+                               if (log.isDebugEnabled())
+                                       log.debug("Unregistered image " + previousResourceName);
+                       }
+                       return getImageUrl(fileNode);
+               } catch (IOException e) {
+                       throw new CmsException("Cannot upload image " + fileName + " in "
+                                       + parentNode, e);
+               } finally {
+                       IOUtils.closeQuietly(inputStream);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/JcrContentProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/JcrContentProvider.java
new file mode 100644 (file)
index 0000000..44885b1
--- /dev/null
@@ -0,0 +1,81 @@
+package org.argeo.cms.ui.internal;
+
+import java.util.ArrayList;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.CmsException;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+@Deprecated
+class JcrContentProvider implements ITreeContentProvider {
+       private static final long serialVersionUID = -1333678161322488674L;
+
+       @Override
+       public void dispose() {
+       }
+
+       @Override
+       public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               if (newInput == null)
+                       return;
+               if (!(newInput instanceof Node))
+                       throw new CmsException("Input " + newInput + " must be a node");
+       }
+
+       @Override
+       public Object[] getElements(Object inputElement) {
+               try {
+                       Node node = (Node) inputElement;
+                       ArrayList<Node> arr = new ArrayList<Node>();
+                       NodeIterator nit = node.getNodes();
+                       while (nit.hasNext()) {
+                               arr.add(nit.nextNode());
+                       }
+                       return arr.toArray();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot get elements", e);
+               }
+       }
+
+       @Override
+       public Object[] getChildren(Object parentElement) {
+               try {
+                       Node node = (Node) parentElement;
+                       ArrayList<Node> arr = new ArrayList<Node>();
+                       NodeIterator nit = node.getNodes();
+                       while (nit.hasNext()) {
+                               arr.add(nit.nextNode());
+                       }
+                       return arr.toArray();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot get elements", e);
+               }
+       }
+
+       @Override
+       public Object getParent(Object element) {
+               try {
+                       Node node = (Node) element;
+                       if (node.getName().equals(""))
+                               return null;
+                       else
+                               return node.getParent();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot get elements", e);
+               }
+       }
+
+       @Override
+       public boolean hasChildren(Object element) {
+               try {
+                       Node node = (Node) element;
+                       return node.hasNodes();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot get elements", e);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/JcrFileUploadReceiver.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/JcrFileUploadReceiver.java
new file mode 100644 (file)
index 0000000..47eb191
--- /dev/null
@@ -0,0 +1,82 @@
+package org.argeo.cms.ui.internal;
+
+import static javax.jcr.nodetype.NodeType.NT_FILE;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.FilenameUtils;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.ui.CmsImageManager;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.rap.fileupload.FileDetails;
+import org.eclipse.rap.fileupload.FileUploadReceiver;
+
+public class JcrFileUploadReceiver extends FileUploadReceiver implements
+               CmsNames {
+       private final Node parentNode;
+       private final String nodeName;
+       private final CmsImageManager imageManager;
+
+       /** If nodeName is null, use the uploaded file name */
+       public JcrFileUploadReceiver(Node parentNode, String nodeName,
+                       CmsImageManager imageManager) {
+               super();
+               this.parentNode = parentNode;
+               this.nodeName = nodeName;
+               this.imageManager = imageManager;
+       }
+
+       @Override
+       public void receive(InputStream stream, FileDetails details)
+                       throws IOException {
+               try {
+                       String fileName = nodeName != null ? nodeName : details
+                                       .getFileName();
+                       String contentType = details.getContentType();
+                       if (isImage(details.getFileName(), contentType)) {
+                               imageManager.uploadImage(parentNode, fileName, stream);
+                               return;
+                               // InputStream inputStream = new ByteArrayInputStream(arr);
+                               // ImageData id = new ImageData(inputStream);
+                               // fileNode.addMixin(CmsTypes.CMS_IMAGE);
+                               // fileNode.setProperty(CMS_IMAGE_WIDTH, id.width);
+                               // fileNode.setProperty(CMS_IMAGE_HEIGHT, id.height);
+                       }
+
+                       Node fileNode;
+                       if (parentNode.hasNode(fileName)) {
+                               fileNode = parentNode.getNode(fileName);
+                               if (!fileNode.isNodeType(NT_FILE))
+                                       fileNode.remove();
+                       }
+                       fileNode = JcrUtils.copyStreamAsFile(parentNode, fileName, stream);
+
+                       if (contentType != null) {
+                               fileNode.addMixin(NodeType.MIX_MIMETYPE);
+                               fileNode.setProperty(Property.JCR_MIMETYPE, contentType);
+                       }
+                       processNewFile(fileNode);
+                       fileNode.getSession().save();
+               } catch (RepositoryException e) {
+                       throw new CmsException("cannot receive " + details, e);
+               }
+       }
+
+       protected Boolean isImage(String fileName, String contentType) {
+               String ext = FilenameUtils.getExtension(fileName);
+               return ext != null
+                               && (ext.equals("png") || ext.equalsIgnoreCase("jpg"));
+       }
+
+       protected void processNewFile(Node node) {
+
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/SimpleEditableImage.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/SimpleEditableImage.java
new file mode 100644 (file)
index 0000000..3bb1fdf
--- /dev/null
@@ -0,0 +1,73 @@
+package org.argeo.cms.ui.internal;
+
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.widgets.EditableImage;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+
+/** NOT working yet. */
+public class SimpleEditableImage extends EditableImage {
+       private static final long serialVersionUID = -5689145523114022890L;
+
+       private String src;
+       private Point imageSize;
+
+       public SimpleEditableImage(Composite parent, int swtStyle) {
+               super(parent, swtStyle);
+               // load(getControl());
+               getParent().layout();
+       }
+
+       public SimpleEditableImage(Composite parent, int swtStyle, String src,
+                       Point imageSize) {
+               super(parent, swtStyle);
+               this.src = src;
+               this.imageSize = imageSize;
+       }
+
+       @Override
+       protected Control createControl(Composite box, String style) {
+               if (isEditing()) {
+                       return createText(box, style);
+               } else {
+                       return createLabel(box, style);
+               }
+       }
+
+       protected String createImgTag() throws RepositoryException {
+               String imgTag;
+               if (src != null)
+                       imgTag = CmsUtils.img(src, imageSize);
+               else
+                       imgTag = CmsUtils.noImg(imageSize != null ? imageSize
+                                       : NO_IMAGE_SIZE);
+               return imgTag;
+       }
+
+       protected Text createText(Composite box, String style) {
+               Text text = new Text(box, getStyle());
+               CmsUtils.style(text, style);
+               return text;
+       }
+
+       public String getSrc() {
+               return src;
+       }
+
+       public void setSrc(String src) {
+               this.src = src;
+       }
+
+       public Point getImageSize() {
+               return imageSize;
+       }
+
+       public void setImageSize(Point imageSize) {
+               this.imageSize = imageSize;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/rwt/UserUi.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/rwt/UserUi.java
new file mode 100644 (file)
index 0000000..88cd17b
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.cms.ui.internal.rwt;
+
+import org.argeo.cms.util.LoginEntryPoint;
+import org.eclipse.rap.rwt.application.Application;
+import org.eclipse.rap.rwt.application.Application.OperationMode;
+import org.eclipse.rap.rwt.application.ApplicationConfiguration;
+
+public class UserUi implements ApplicationConfiguration {
+       @Override
+       public void configure(Application application) {
+               application.setOperationMode(OperationMode.SWT_COMPATIBILITY);
+               application.addEntryPoint("/login", LoginEntryPoint.class, null);
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/AbstractDbkViewer.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/AbstractDbkViewer.java
new file mode 100644 (file)
index 0000000..b75aa3e
--- /dev/null
@@ -0,0 +1,837 @@
+package org.argeo.cms.ui.internal.text;
+
+import static javax.jcr.Property.JCR_TITLE;
+import static org.argeo.cms.util.CmsUtils.fillWidth;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Observer;
+
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.text.DbkTextInterpreter;
+import org.argeo.cms.text.Img;
+import org.argeo.cms.text.Paragraph;
+import org.argeo.cms.text.TextInterpreter;
+import org.argeo.cms.text.TextSection;
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.ui.CmsImageManager;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.AbstractPageViewer;
+import org.argeo.cms.viewers.EditablePart;
+import org.argeo.cms.viewers.NodePart;
+import org.argeo.cms.viewers.PropertyPart;
+import org.argeo.cms.viewers.Section;
+import org.argeo.cms.viewers.SectionPart;
+import org.argeo.cms.widgets.EditableImage;
+import org.argeo.cms.widgets.EditableText;
+import org.argeo.cms.widgets.StyledControl;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.docbook.DocBookNames;
+import org.argeo.jcr.docbook.DocBookTypes;
+import org.eclipse.rap.fileupload.FileDetails;
+import org.eclipse.rap.fileupload.FileUploadEvent;
+import org.eclipse.rap.fileupload.FileUploadHandler;
+import org.eclipse.rap.fileupload.FileUploadListener;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+
+/** Base class for text viewers and editors. */
+public abstract class AbstractDbkViewer extends AbstractPageViewer implements KeyListener, Observer {
+       private static final long serialVersionUID = -2401274679492339668L;
+       private final static Log log = LogFactory.getLog(AbstractDbkViewer.class);
+
+       private final Section mainSection;
+
+       private TextInterpreter textInterpreter = new DbkTextInterpreter();
+       private CmsImageManager imageManager = CmsUtils.getCmsView().getImageManager();
+
+       private FileUploadListener fileUploadListener;
+       private DbkContextMenu styledTools;
+
+       private final boolean flat;
+
+       protected AbstractDbkViewer(Section parent, int style, CmsEditable cmsEditable) {
+               super(parent, style, cmsEditable);
+               flat = SWT.FLAT == (style & SWT.FLAT);
+
+               if (getCmsEditable().canEdit()) {
+                       fileUploadListener = new FUL();
+                       styledTools = new DbkContextMenu(this, parent.getDisplay());
+               }
+               this.mainSection = parent;
+               initModelIfNeeded(mainSection.getNode());
+               // layout(this.mainSection);
+       }
+
+       @Override
+       public Control getControl() {
+               return mainSection;
+       }
+
+       protected void refresh(Control control) throws RepositoryException {
+               if (!(control instanceof Section))
+                       return;
+               Section section = (Section) control;
+               if (section instanceof TextSection) {
+                       CmsUtils.clear(section);
+                       Node node = section.getNode();
+                       TextSection textSection = (TextSection) section;
+                       if (node.hasProperty(Property.JCR_TITLE)) {
+                               if (section.getHeader() == null)
+                                       section.createHeader();
+                               if (node.hasProperty(Property.JCR_TITLE)) {
+                                       SectionTitle title = newSectionTitle(textSection, node);
+                                       title.setLayoutData(CmsUtils.fillWidth());
+                                       updateContent(title);
+                               }
+                       }
+
+                       for (NodeIterator ni = node.getNodes(DocBookNames.DBK_PARA); ni.hasNext();) {
+                               Node child = ni.nextNode();
+                               final SectionPart sectionPart;
+                               if (child.isNodeType(DocBookTypes.IMAGEDATA) || child.isNodeType(NodeType.NT_FILE)) {
+                                       // FIXME adapt to DocBook
+                                       sectionPart = newImg(textSection, child);
+                               } else if (child.isNodeType(DocBookTypes.PARA)) {
+                                       sectionPart = newParagraph(textSection, child);
+                               } else {
+                                       sectionPart = newSectionPart(textSection, child);
+                                       if (sectionPart == null)
+                                               throw new CmsException("Unsupported node " + child);
+                                       // TODO list node types in exception
+                               }
+                               if (sectionPart instanceof Control)
+                                       ((Control) sectionPart).setLayoutData(CmsUtils.fillWidth());
+                       }
+
+                       if (!flat)
+                               for (NodeIterator ni = section.getNode().getNodes(DocBookNames.DBK_SECTION); ni.hasNext();) {
+                                       Node child = ni.nextNode();
+                                       if (child.isNodeType(DocBookTypes.SECTION)) {
+                                               TextSection newSection = new TextSection(section, SWT.NONE, child);
+                                               newSection.setLayoutData(CmsUtils.fillWidth());
+                                               refresh(newSection);
+                                       }
+                               }
+               } else {
+                       for (Section s : section.getSubSections().values())
+                               refresh(s);
+               }
+               // section.layout();
+       }
+
+       /** To be overridden in order to provide additional SectionPart types */
+       protected SectionPart newSectionPart(TextSection textSection, Node node) {
+               return null;
+       }
+
+       // CRUD
+       protected Paragraph newParagraph(TextSection parent, Node node) throws RepositoryException {
+               Paragraph paragraph = new Paragraph(parent, parent.getStyle(), node);
+               updateContent(paragraph);
+               paragraph.setLayoutData(fillWidth());
+               paragraph.setMouseListener(getMouseListener());
+               return paragraph;
+       }
+
+       protected Img newImg(TextSection parent, Node node) throws RepositoryException {
+               Img img = new Img(parent, parent.getStyle(), node) {
+                       private static final long serialVersionUID = 1297900641952417540L;
+
+                       @Override
+                       protected void setContainerLayoutData(Composite composite) {
+                               composite.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
+                       }
+
+                       @Override
+                       protected void setControlLayoutData(Control control) {
+                               control.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
+                       }
+               };
+               img.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
+               updateContent(img);
+               img.setMouseListener(getMouseListener());
+               return img;
+       }
+
+       protected SectionTitle newSectionTitle(TextSection parent, Node node) throws RepositoryException {
+               SectionTitle title = new SectionTitle(parent.getHeader(), parent.getStyle(), node.getProperty(JCR_TITLE));
+               updateContent(title);
+               title.setMouseListener(getMouseListener());
+               return title;
+       }
+
+       protected SectionTitle prepareSectionTitle(Section newSection, String titleText) throws RepositoryException {
+               Node sectionNode = newSection.getNode();
+               if (!sectionNode.hasProperty(JCR_TITLE))
+                       sectionNode.setProperty(Property.JCR_TITLE, "");
+               getTextInterpreter().write(sectionNode.getProperty(Property.JCR_TITLE), titleText);
+               if (newSection.getHeader() == null)
+                       newSection.createHeader();
+               SectionTitle sectionTitle = newSectionTitle((TextSection) newSection, sectionNode);
+               return sectionTitle;
+       }
+
+       protected void updateContent(EditablePart part) throws RepositoryException {
+               if (part instanceof SectionPart) {
+                       SectionPart sectionPart = (SectionPart) part;
+                       Node partNode = sectionPart.getNode();
+
+                       if (part instanceof StyledControl && (sectionPart.getSection() instanceof TextSection)) {
+                               TextSection section = (TextSection) sectionPart.getSection();
+                               StyledControl styledControl = (StyledControl) part;
+                               if (partNode.isNodeType(DocBookTypes.PARA)) {
+                                       String style = partNode.hasProperty(DocBookNames.DBK_ROLE)
+                                                       ? partNode.getProperty(DocBookNames.DBK_ROLE).getString()
+                                                       : section.getDefaultTextStyle();
+                                       styledControl.setStyle(style);
+                               }
+                       }
+                       // use control AFTER setting style, since it may have been reset
+
+                       if (part instanceof EditableText) {
+                               EditableText paragraph = (EditableText) part;
+                               if (paragraph == getEdited())
+                                       paragraph.setText(textInterpreter.read(partNode));
+                               else
+                                       paragraph.setText(textInterpreter.raw(partNode));
+                       } else if (part instanceof EditableImage) {
+                               EditableImage editableImage = (EditableImage) part;
+                               imageManager.load(partNode, part.getControl(), editableImage.getPreferredImageSize());
+                       }
+               } else if (part instanceof SectionTitle) {
+                       SectionTitle title = (SectionTitle) part;
+                       title.setStyle(title.getSection().getTitleStyle());
+                       // use control AFTER setting style
+                       if (title == getEdited())
+                               title.setText(textInterpreter.read(title.getProperty()));
+                       else
+                               title.setText(textInterpreter.raw(title.getProperty()));
+               }
+       }
+
+       // OVERRIDDEN FROM PARENT VIEWER
+       @Override
+       protected void save(EditablePart part) throws RepositoryException {
+               if (part instanceof EditableText) {
+                       EditableText et = (EditableText) part;
+                       String text = ((Text) et.getControl()).getText();
+
+                       String[] lines = text.split("[\r\n]+");
+                       assert lines.length != 0;
+                       saveLine(part, lines[0]);
+                       if (lines.length > 1) {
+                               ArrayList<Control> toLayout = new ArrayList<Control>();
+                               if (part instanceof Paragraph) {
+                                       Paragraph currentParagraph = (Paragraph) et;
+                                       Section section = currentParagraph.getSection();
+                                       Node sectionNode = section.getNode();
+                                       Node currentParagraphN = currentParagraph.getNode();
+                                       for (int i = 1; i < lines.length; i++) {
+                                               Node newNode = sectionNode.addNode(DocBookNames.DBK_PARA, DocBookTypes.PARA);
+                                               // newNode.addMixin(CmsTypes.CMS_STYLED);
+                                               saveLine(newNode, lines[i]);
+                                               // second node was create as last, if it is not the next
+                                               // one, it
+                                               // means there are some in between and we can take the
+                                               // one at
+                                               // index+1 for the re-order
+                                               if (newNode.getIndex() > currentParagraphN.getIndex() + 1) {
+                                                       sectionNode.orderBefore(p(newNode.getIndex()), p(currentParagraphN.getIndex() + 1));
+                                               }
+                                               Paragraph newParagraph = newParagraph((TextSection) section, newNode);
+                                               newParagraph.moveBelow(currentParagraph);
+                                               toLayout.add(newParagraph);
+
+                                               currentParagraph = newParagraph;
+                                               currentParagraphN = newNode;
+                                       }
+                                       persistChanges(sectionNode);
+                               }
+                               // TODO or rather return the created paragarphs?
+                               layout(toLayout.toArray(new Control[toLayout.size()]));
+                       }
+               }
+       }
+
+       protected void saveLine(EditablePart part, String line) {
+               if (part instanceof NodePart) {
+                       saveLine(((NodePart) part).getNode(), line);
+               } else if (part instanceof PropertyPart) {
+                       saveLine(((PropertyPart) part).getProperty(), line);
+               } else {
+                       throw new CmsException("Unsupported part " + part);
+               }
+       }
+
+       protected void saveLine(Item item, String line) {
+               line = line.trim();
+               textInterpreter.write(item, line);
+       }
+
+       @Override
+       protected void prepare(EditablePart part, Object caretPosition) {
+               Control control = part.getControl();
+               if (control instanceof Text) {
+                       Text text = (Text) control;
+                       if (caretPosition != null)
+                               if (caretPosition instanceof Integer)
+                                       text.setSelection((Integer) caretPosition);
+                               else if (caretPosition instanceof Point) {
+                                       // TODO find a way to position the caret at the right place
+                               }
+                       text.setData(RWT.ACTIVE_KEYS, new String[] { "BACKSPACE", "ESC", "TAB", "SHIFT+TAB", "ALT+ARROW_LEFT",
+                                       "ALT+ARROW_RIGHT", "ALT+ARROW_UP", "ALT+ARROW_DOWN", "RETURN", "CTRL+RETURN", "ENTER", "DELETE" });
+                       text.setData(RWT.CANCEL_KEYS, new String[] { "RETURN", "ALT+ARROW_LEFT", "ALT+ARROW_RIGHT" });
+                       text.addKeyListener(this);
+               } else if (part instanceof Img) {
+                       ((Img) part).setFileUploadListener(fileUploadListener);
+               }
+       }
+
+       // REQUIRED BY CONTEXT MENU
+       void setParagraphStyle(Paragraph paragraph, String style) {
+               try {
+                       Node paragraphNode = paragraph.getNode();
+                       paragraphNode.setProperty(DocBookNames.DBK_ROLE, style);
+                       persistChanges(paragraphNode);
+                       updateContent(paragraph);
+                       layout(paragraph);
+               } catch (RepositoryException e1) {
+                       throw new CmsException("Cannot set style " + style + " on " + paragraph, e1);
+               }
+       }
+
+       void deletePart(SectionPart paragraph) {
+               try {
+                       Node paragraphNode = paragraph.getNode();
+                       Section section = paragraph.getSection();
+                       Session session = paragraphNode.getSession();
+                       paragraphNode.remove();
+                       session.save();
+                       if (paragraph instanceof Control)
+                               ((Control) paragraph).dispose();
+                       layout(section);
+               } catch (RepositoryException e1) {
+                       throw new CmsException("Cannot delete " + paragraph, e1);
+               }
+       }
+
+       String getRawParagraphText(Paragraph paragraph) {
+               return textInterpreter.raw(paragraph.getNode());
+       }
+
+       // COMMANDS
+       protected void splitEdit() {
+               checkEdited();
+               try {
+                       if (getEdited() instanceof Paragraph) {
+                               Paragraph paragraph = (Paragraph) getEdited();
+                               Text text = (Text) paragraph.getControl();
+                               int caretPosition = text.getCaretPosition();
+                               String txt = text.getText();
+                               String first = txt.substring(0, caretPosition);
+                               String second = txt.substring(caretPosition);
+                               Node firstNode = paragraph.getNode();
+                               Node sectionNode = firstNode.getParent();
+
+                               // FIXME set content the DocBook way
+                               // firstNode.setProperty(CMS_CONTENT, first);
+                               Node secondNode = sectionNode.addNode(DocBookNames.DBK_PARA, DocBookTypes.PARA);
+                               // secondNode.addMixin(CmsTypes.CMS_STYLED);
+
+                               // second node was create as last, if it is not the next one, it
+                               // means there are some in between and we can take the one at
+                               // index+1 for the re-order
+                               if (secondNode.getIndex() > firstNode.getIndex() + 1) {
+                                       sectionNode.orderBefore(p(secondNode.getIndex()), p(firstNode.getIndex() + 1));
+                               }
+
+                               // if we die in between, at least we still have the whole text
+                               // in the first node
+                               try {
+                                       textInterpreter.write(secondNode, second);
+                                       textInterpreter.write(firstNode, first);
+                               } catch (Exception e) {
+                                       // so that no additional nodes are created:
+                                       JcrUtils.discardUnderlyingSessionQuietly(firstNode);
+                                       throw e;
+                               }
+
+                               persistChanges(firstNode);
+
+                               Paragraph secondParagraph = paragraphSplitted(paragraph, secondNode);
+                               edit(secondParagraph, 0);
+                       } else if (getEdited() instanceof SectionTitle) {
+                               SectionTitle sectionTitle = (SectionTitle) getEdited();
+                               Text text = (Text) sectionTitle.getControl();
+                               String txt = text.getText();
+                               int caretPosition = text.getCaretPosition();
+                               Section section = sectionTitle.getSection();
+                               Node sectionNode = section.getNode();
+                               Node paragraphNode = sectionNode.addNode(DocBookNames.DBK_PARA, DocBookTypes.PARA);
+                               // paragraphNode.addMixin(CmsTypes.CMS_STYLED);
+
+                               textInterpreter.write(paragraphNode, txt.substring(caretPosition));
+                               textInterpreter.write(sectionNode.getProperty(Property.JCR_TITLE), txt.substring(0, caretPosition));
+                               sectionNode.orderBefore(p(paragraphNode.getIndex()), p(1));
+                               persistChanges(sectionNode);
+
+                               Paragraph paragraph = sectionTitleSplitted(sectionTitle, paragraphNode);
+                               // section.layout();
+                               edit(paragraph, 0);
+                       }
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot split " + getEdited(), e);
+               }
+       }
+
+       protected void mergeWithPrevious() {
+               checkEdited();
+               try {
+                       Paragraph paragraph = (Paragraph) getEdited();
+                       Text text = (Text) paragraph.getControl();
+                       String txt = text.getText();
+                       Node paragraphNode = paragraph.getNode();
+                       if (paragraphNode.getIndex() == 1)
+                               return;// do nothing
+                       Node sectionNode = paragraphNode.getParent();
+                       Node previousNode = sectionNode.getNode(p(paragraphNode.getIndex() - 1));
+                       String previousTxt = textInterpreter.read(previousNode);
+                       textInterpreter.write(previousNode, previousTxt + txt);
+                       paragraphNode.remove();
+                       persistChanges(sectionNode);
+
+                       Paragraph previousParagraph = paragraphMergedWithPrevious(paragraph, previousNode);
+                       edit(previousParagraph, previousTxt.length());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot stop editing", e);
+               }
+       }
+
+       protected void mergeWithNext() {
+               checkEdited();
+               try {
+                       Paragraph paragraph = (Paragraph) getEdited();
+                       Text text = (Text) paragraph.getControl();
+                       String txt = text.getText();
+                       Node paragraphNode = paragraph.getNode();
+                       Node sectionNode = paragraphNode.getParent();
+                       NodeIterator paragraphNodes = sectionNode.getNodes(DocBookNames.DBK_PARA);
+                       long size = paragraphNodes.getSize();
+                       if (paragraphNode.getIndex() == size)
+                               return;// do nothing
+                       Node nextNode = sectionNode.getNode(p(paragraphNode.getIndex() + 1));
+                       String nextTxt = textInterpreter.read(nextNode);
+                       textInterpreter.write(paragraphNode, txt + nextTxt);
+
+                       Section section = paragraph.getSection();
+                       Paragraph removed = (Paragraph) section.getSectionPart(nextNode.getIdentifier());
+
+                       nextNode.remove();
+                       persistChanges(sectionNode);
+
+                       paragraphMergedWithNext(paragraph, removed);
+                       edit(paragraph, txt.length());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot stop editing", e);
+               }
+       }
+
+       protected synchronized void upload(EditablePart part) {
+               try {
+                       if (part instanceof SectionPart) {
+                               SectionPart sectionPart = (SectionPart) part;
+                               Node partNode = sectionPart.getNode();
+                               int partIndex = partNode.getIndex();
+                               Section section = sectionPart.getSection();
+                               Node sectionNode = section.getNode();
+
+                               if (part instanceof Paragraph) {
+                                       // FIXME adapt to DocBook
+                                       Node newNode = sectionNode.addNode(DocBookNames.DBK_MEDIAOBJECT, NodeType.NT_FILE);
+                                       newNode.addNode(Node.JCR_CONTENT, NodeType.NT_RESOURCE);
+                                       JcrUtils.copyBytesAsFile(sectionNode, p(newNode.getIndex()), new byte[0]);
+                                       if (partIndex < newNode.getIndex() - 1) {
+                                               // was not last
+                                               sectionNode.orderBefore(p(newNode.getIndex()), p(partIndex - 1));
+                                       }
+                                       // sectionNode.orderBefore(p(partNode.getIndex()),
+                                       // p(newNode.getIndex()));
+                                       persistChanges(sectionNode);
+                                       Img img = newImg((TextSection) section, newNode);
+                                       edit(img, null);
+                                       layout(img.getControl());
+                               } else if (part instanceof Img) {
+                                       if (getEdited() == part)
+                                               return;
+                                       edit(part, null);
+                                       layout(part.getControl());
+                               }
+                       }
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot upload", e);
+               }
+       }
+
+       protected void deepen() {
+               if (flat)
+                       return;
+               checkEdited();
+               try {
+                       if (getEdited() instanceof Paragraph) {
+                               Paragraph paragraph = (Paragraph) getEdited();
+                               Text text = (Text) paragraph.getControl();
+                               String txt = text.getText();
+                               Node paragraphNode = paragraph.getNode();
+                               Section section = paragraph.getSection();
+                               Node sectionNode = section.getNode();
+                               // main title
+                               if (section == mainSection && section instanceof TextSection && paragraphNode.getIndex() == 1
+                                               && !sectionNode.hasProperty(JCR_TITLE)) {
+                                       SectionTitle sectionTitle = prepareSectionTitle(section, txt);
+                                       edit(sectionTitle, 0);
+                                       return;
+                               }
+                               Node newSectionNode = sectionNode.addNode(DocBookNames.DBK_SECTION, DocBookTypes.SECTION);
+                               newSectionNode.addMixin(NodeType.MIX_TITLE);
+                               sectionNode.orderBefore(h(newSectionNode.getIndex()), h(1));
+
+                               int paragraphIndex = paragraphNode.getIndex();
+                               String sectionPath = sectionNode.getPath();
+                               String newSectionPath = newSectionNode.getPath();
+                               while (sectionNode.hasNode(p(paragraphIndex + 1))) {
+                                       Node parag = sectionNode.getNode(p(paragraphIndex + 1));
+                                       sectionNode.getSession().move(sectionPath + '/' + p(paragraphIndex + 1),
+                                                       newSectionPath + '/' + DocBookNames.DBK_PARA);
+                                       SectionPart sp = section.getSectionPart(parag.getIdentifier());
+                                       if (sp instanceof Control)
+                                               ((Control) sp).dispose();
+                               }
+                               // create property
+                               newSectionNode.setProperty(Property.JCR_TITLE, "");
+                               getTextInterpreter().write(newSectionNode.getProperty(Property.JCR_TITLE), txt);
+
+                               TextSection newSection = new TextSection(section, section.getStyle(), newSectionNode);
+                               newSection.setLayoutData(CmsUtils.fillWidth());
+                               newSection.moveBelow(paragraph);
+
+                               // dispose
+                               paragraphNode.remove();
+                               paragraph.dispose();
+
+                               refresh(newSection);
+                               newSection.getParent().layout();
+                               layout(newSection);
+                               persistChanges(sectionNode);
+                       } else if (getEdited() instanceof SectionTitle) {
+                               SectionTitle sectionTitle = (SectionTitle) getEdited();
+                               Section section = sectionTitle.getSection();
+                               Section parentSection = section.getParentSection();
+                               if (parentSection == null)
+                                       return;// cannot deepen main section
+                               Node sectionN = section.getNode();
+                               Node parentSectionN = parentSection.getNode();
+                               if (sectionN.getIndex() == 1)
+                                       return;// cannot deepen first section
+                               Node previousSectionN = parentSectionN.getNode(h(sectionN.getIndex() - 1));
+                               NodeIterator subSections = previousSectionN.getNodes(DocBookNames.DBK_SECTION);
+                               int subsectionsCount = (int) subSections.getSize();
+                               previousSectionN.getSession().move(sectionN.getPath(),
+                                               previousSectionN.getPath() + "/" + h(subsectionsCount + 1));
+                               section.dispose();
+                               TextSection newSection = new TextSection(section, section.getStyle(), sectionN);
+                               refresh(newSection);
+                               persistChanges(previousSectionN);
+                       }
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot deepen " + getEdited(), e);
+               }
+       }
+
+       protected void undeepen() {
+               if (flat)
+                       return;
+               checkEdited();
+               try {
+                       if (getEdited() instanceof Paragraph) {
+                               upload(getEdited());
+                       } else if (getEdited() instanceof SectionTitle) {
+                               SectionTitle sectionTitle = (SectionTitle) getEdited();
+                               Section section = sectionTitle.getSection();
+                               Node sectionNode = section.getNode();
+                               Section parentSection = section.getParentSection();
+                               if (parentSection == null)
+                                       return;// cannot undeepen main section
+
+                               // choose in which section to merge
+                               Section mergedSection;
+                               if (sectionNode.getIndex() == 1)
+                                       mergedSection = section.getParentSection();
+                               else {
+                                       Map<String, Section> parentSubsections = parentSection.getSubSections();
+                                       ArrayList<Section> lst = new ArrayList<Section>(parentSubsections.values());
+                                       mergedSection = lst.get(sectionNode.getIndex() - 1);
+                               }
+                               Node mergedNode = mergedSection.getNode();
+                               boolean mergedHasSubSections = mergedNode.hasNode(DocBookNames.DBK_SECTION);
+
+                               // title as paragraph
+                               Node newParagrapheNode = mergedNode.addNode(DocBookNames.DBK_PARA, DocBookTypes.PARA);
+                               // newParagrapheNode.addMixin(CmsTypes.CMS_STYLED);
+                               if (mergedHasSubSections)
+                                       mergedNode.orderBefore(p(newParagrapheNode.getIndex()), h(1));
+                               String txt = getTextInterpreter().read(sectionNode.getProperty(Property.JCR_TITLE));
+                               getTextInterpreter().write(newParagrapheNode, txt);
+                               // move
+                               NodeIterator paragraphs = sectionNode.getNodes(DocBookNames.DBK_PARA);
+                               while (paragraphs.hasNext()) {
+                                       Node p = paragraphs.nextNode();
+                                       SectionPart sp = section.getSectionPart(p.getIdentifier());
+                                       if (sp instanceof Control)
+                                               ((Control) sp).dispose();
+                                       mergedNode.getSession().move(p.getPath(), mergedNode.getPath() + '/' + DocBookNames.DBK_PARA);
+                                       if (mergedHasSubSections)
+                                               mergedNode.orderBefore(p(p.getIndex()), h(1));
+                               }
+
+                               Iterator<Section> subsections = section.getSubSections().values().iterator();
+                               // NodeIterator sections = sectionNode.getNodes(CMS_H);
+                               while (subsections.hasNext()) {
+                                       Section subsection = subsections.next();
+                                       Node s = subsection.getNode();
+                                       mergedNode.getSession().move(s.getPath(), mergedNode.getPath() + '/' + DocBookNames.DBK_SECTION);
+                                       subsection.dispose();
+                               }
+
+                               // remove section
+                               section.getNode().remove();
+                               section.dispose();
+
+                               refresh(mergedSection);
+                               mergedSection.getParent().layout();
+                               layout(mergedSection);
+                               persistChanges(mergedNode);
+                       }
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot undeepen " + getEdited(), e);
+               }
+       }
+
+       // UI CHANGES
+       protected Paragraph paragraphSplitted(Paragraph paragraph, Node newNode) throws RepositoryException {
+               Section section = paragraph.getSection();
+               updateContent(paragraph);
+               Paragraph newParagraph = newParagraph((TextSection) section, newNode);
+               newParagraph.setLayoutData(CmsUtils.fillWidth());
+               newParagraph.moveBelow(paragraph);
+               layout(paragraph.getControl(), newParagraph.getControl());
+               return newParagraph;
+       }
+
+       protected Paragraph sectionTitleSplitted(SectionTitle sectionTitle, Node newNode) throws RepositoryException {
+               updateContent(sectionTitle);
+               Paragraph newParagraph = newParagraph(sectionTitle.getSection(), newNode);
+               // we assume beforeFirst is not null since there was a sectionTitle
+               newParagraph.moveBelow(sectionTitle.getSection().getHeader());
+               layout(sectionTitle.getControl(), newParagraph.getControl());
+               return newParagraph;
+       }
+
+       protected Paragraph paragraphMergedWithPrevious(Paragraph removed, Node remaining) throws RepositoryException {
+               Section section = removed.getSection();
+               removed.dispose();
+
+               Paragraph paragraph = (Paragraph) section.getSectionPart(remaining.getIdentifier());
+               updateContent(paragraph);
+               layout(paragraph.getControl());
+               return paragraph;
+       }
+
+       protected void paragraphMergedWithNext(Paragraph remaining, Paragraph removed) throws RepositoryException {
+               removed.dispose();
+               updateContent(remaining);
+               layout(remaining.getControl());
+       }
+
+       // UTILITIES
+       protected String p(Integer index) {
+               StringBuilder sb = new StringBuilder(6);
+               sb.append(DocBookNames.DBK_PARA).append('[').append(index).append(']');
+               return sb.toString();
+       }
+
+       protected String h(Integer index) {
+               StringBuilder sb = new StringBuilder(5);
+               sb.append(DocBookNames.DBK_SECTION).append('[').append(index).append(']');
+               return sb.toString();
+       }
+
+       // GETTERS / SETTERS
+       public Section getMainSection() {
+               return mainSection;
+       }
+
+       public boolean isFlat() {
+               return flat;
+       }
+
+       public TextInterpreter getTextInterpreter() {
+               return textInterpreter;
+       }
+
+       // KEY LISTENER
+       @Override
+       public void keyPressed(KeyEvent ke) {
+               if (log.isTraceEnabled())
+                       log.trace(ke);
+
+               if (getEdited() == null)
+                       return;
+               boolean altPressed = (ke.stateMask & SWT.ALT) != 0;
+               boolean shiftPressed = (ke.stateMask & SWT.SHIFT) != 0;
+               boolean ctrlPressed = (ke.stateMask & SWT.CTRL) != 0;
+
+               try {
+                       // Common
+                       if (ke.keyCode == SWT.ESC) {
+                               cancelEdit();
+                       } else if (ke.character == '\r') {
+                               splitEdit();
+                       } else if (ke.character == 'S') {
+                               if (ctrlPressed)
+                                       saveEdit();
+                       } else if (ke.character == '\t') {
+                               if (!shiftPressed) {
+                                       deepen();
+                               } else if (shiftPressed) {
+                                       undeepen();
+                               }
+                       } else {
+                               if (getEdited() instanceof Paragraph) {
+                                       Paragraph paragraph = (Paragraph) getEdited();
+                                       Section section = paragraph.getSection();
+                                       if (altPressed && ke.keyCode == SWT.ARROW_RIGHT) {
+                                               edit(section.nextSectionPart(paragraph), 0);
+                                       } else if (altPressed && ke.keyCode == SWT.ARROW_LEFT) {
+                                               edit(section.previousSectionPart(paragraph), 0);
+                                       } else if (ke.character == SWT.BS) {
+                                               Text text = (Text) paragraph.getControl();
+                                               int caretPosition = text.getCaretPosition();
+                                               if (caretPosition == 0) {
+                                                       mergeWithPrevious();
+                                               }
+                                       } else if (ke.character == SWT.DEL) {
+                                               Text text = (Text) paragraph.getControl();
+                                               int caretPosition = text.getCaretPosition();
+                                               int charcount = text.getCharCount();
+                                               if (caretPosition == charcount) {
+                                                       mergeWithNext();
+                                               }
+                                       }
+                               }
+                       }
+               } catch (Exception e) {
+                       ke.doit = false;
+                       notifyEditionException(e);
+               }
+       }
+
+       @Override
+       public void keyReleased(KeyEvent e) {
+       }
+
+       // MOUSE LISTENER
+       @Override
+       protected MouseListener createMouseListener() {
+               return new ML();
+       }
+
+       private class ML extends MouseAdapter {
+               private static final long serialVersionUID = 8526890859876770905L;
+
+               @Override
+               public void mouseDoubleClick(MouseEvent e) {
+                       if (e.button == 1) {
+                               Control source = (Control) e.getSource();
+                               if (getCmsEditable().canEdit()) {
+                                       if (getCmsEditable().isEditing() && !(getEdited() instanceof Img)) {
+                                               if (source == mainSection)
+                                                       return;
+                                               EditablePart part = findDataParent(source);
+                                               upload(part);
+                                       } else {
+                                               getCmsEditable().startEditing();
+                                       }
+                               }
+                       }
+               }
+
+               @Override
+               public void mouseDown(MouseEvent e) {
+                       if (getCmsEditable().isEditing()) {
+                               if (e.button == 1) {
+                                       Control source = (Control) e.getSource();
+                                       EditablePart composite = findDataParent(source);
+                                       Point point = new Point(e.x, e.y);
+                                       if (!(composite instanceof Img))
+                                               edit(composite, source.toDisplay(point));
+                               } else if (e.button == 3) {
+                                       EditablePart composite = findDataParent((Control) e.getSource());
+                                       if (styledTools != null)
+                                               styledTools.show(composite, new Point(e.x, e.y));
+                               }
+                       }
+               }
+
+               @Override
+               public void mouseUp(MouseEvent e) {
+               }
+       }
+
+       // FILE UPLOAD LISTENER
+       private class FUL implements FileUploadListener {
+               public void uploadProgress(FileUploadEvent event) {
+                       // TODO Monitor upload progress
+               }
+
+               public void uploadFailed(FileUploadEvent event) {
+                       throw new CmsException("Upload failed " + event, event.getException());
+               }
+
+               public void uploadFinished(FileUploadEvent event) {
+                       for (FileDetails file : event.getFileDetails()) {
+                               if (log.isDebugEnabled())
+                                       log.debug("Received: " + file.getFileName());
+                       }
+                       mainSection.getDisplay().syncExec(new Runnable() {
+                               @Override
+                               public void run() {
+                                       saveEdit();
+                               }
+                       });
+                       FileUploadHandler uploadHandler = (FileUploadHandler) event.getSource();
+                       uploadHandler.dispose();
+               }
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/AbstractTextViewer.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/AbstractTextViewer.java
new file mode 100644 (file)
index 0000000..aabf685
--- /dev/null
@@ -0,0 +1,892 @@
+package org.argeo.cms.ui.internal.text;
+
+import static javax.jcr.Property.JCR_TITLE;
+import static org.argeo.cms.util.CmsUtils.fillWidth;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Observer;
+
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.CmsTypes;
+import org.argeo.cms.text.Img;
+import org.argeo.cms.text.Paragraph;
+import org.argeo.cms.text.TextInterpreter;
+import org.argeo.cms.text.TextSection;
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.ui.CmsImageManager;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.viewers.AbstractPageViewer;
+import org.argeo.cms.viewers.EditablePart;
+import org.argeo.cms.viewers.NodePart;
+import org.argeo.cms.viewers.PropertyPart;
+import org.argeo.cms.viewers.Section;
+import org.argeo.cms.viewers.SectionPart;
+import org.argeo.cms.widgets.EditableImage;
+import org.argeo.cms.widgets.EditableText;
+import org.argeo.cms.widgets.StyledControl;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.rap.fileupload.FileDetails;
+import org.eclipse.rap.fileupload.FileUploadEvent;
+import org.eclipse.rap.fileupload.FileUploadHandler;
+import org.eclipse.rap.fileupload.FileUploadListener;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+
+/** Base class for text viewers and editors. */
+public abstract class AbstractTextViewer extends AbstractPageViewer implements
+               CmsNames, KeyListener, Observer {
+       private static final long serialVersionUID = -2401274679492339668L;
+       private final static Log log = LogFactory.getLog(AbstractTextViewer.class);
+
+       private final Section mainSection;
+
+       private TextInterpreter textInterpreter = new TextInterpreterImpl();
+       private CmsImageManager imageManager = CmsUtils.getCmsView()
+                       .getImageManager();
+
+       private FileUploadListener fileUploadListener;
+       private TextContextMenu styledTools;
+
+       private final boolean flat;
+
+       protected AbstractTextViewer(Section parent, int style,
+                       CmsEditable cmsEditable) {
+               super(parent, style, cmsEditable);
+               flat = SWT.FLAT == (style & SWT.FLAT);
+
+               if (getCmsEditable().canEdit()) {
+                       fileUploadListener = new FUL();
+                       styledTools = new TextContextMenu(this, parent.getDisplay());
+               }
+               this.mainSection = parent;
+               initModelIfNeeded(mainSection.getNode());
+               // layout(this.mainSection);
+       }
+
+       @Override
+       public Control getControl() {
+               return mainSection;
+       }
+
+       protected void refresh(Control control) throws RepositoryException {
+               if (!(control instanceof Section))
+                       return;
+               Section section = (Section) control;
+               if (section instanceof TextSection) {
+                       CmsUtils.clear(section);
+                       Node node = section.getNode();
+                       TextSection textSection = (TextSection) section;
+                       if (node.hasProperty(Property.JCR_TITLE)) {
+                               if (section.getHeader() == null)
+                                       section.createHeader();
+                               if (node.hasProperty(Property.JCR_TITLE)) {
+                                       SectionTitle title = newSectionTitle(textSection, node);
+                                       title.setLayoutData(CmsUtils.fillWidth());
+                                       updateContent(title);
+                               }
+                       }
+
+                       for (NodeIterator ni = node.getNodes(CMS_P); ni.hasNext();) {
+                               Node child = ni.nextNode();
+                               final SectionPart sectionPart;
+                               if (child.isNodeType(CmsTypes.CMS_IMAGE)
+                                               || child.isNodeType(NodeType.NT_FILE)) {
+                                       sectionPart = newImg(textSection, child);
+                               } else if (child.isNodeType(CmsTypes.CMS_STYLED)) {
+                                       sectionPart = newParagraph(textSection, child);
+                               } else {
+                                       sectionPart = newSectionPart(textSection, child);
+                                       if (sectionPart == null)
+                                               throw new CmsException("Unsupported node " + child);
+                                       // TODO list node types in exception
+                               }
+                               if (sectionPart instanceof Control)
+                                       ((Control) sectionPart).setLayoutData(CmsUtils.fillWidth());
+                       }
+
+                       if (!flat)
+                               for (NodeIterator ni = section.getNode().getNodes(CMS_H); ni
+                                               .hasNext();) {
+                                       Node child = ni.nextNode();
+                                       if (child.isNodeType(CmsTypes.CMS_SECTION)) {
+                                               TextSection newSection = new TextSection(section,
+                                                               SWT.NONE, child);
+                                               newSection.setLayoutData(CmsUtils.fillWidth());
+                                               refresh(newSection);
+                                       }
+                               }
+               } else {
+                       for (Section s : section.getSubSections().values())
+                               refresh(s);
+               }
+               // section.layout();
+       }
+
+       /** To be overridden in order to provide additional SectionPart types */
+       protected SectionPart newSectionPart(TextSection textSection, Node node) {
+               return null;
+       }
+
+       // CRUD
+       protected Paragraph newParagraph(TextSection parent, Node node)
+                       throws RepositoryException {
+               Paragraph paragraph = new Paragraph(parent, parent.getStyle(), node);
+               updateContent(paragraph);
+               paragraph.setLayoutData(fillWidth());
+               paragraph.setMouseListener(getMouseListener());
+               return paragraph;
+       }
+
+       protected Img newImg(TextSection parent, Node node)
+                       throws RepositoryException {
+               Img img = new Img(parent, parent.getStyle(), node) {
+                       private static final long serialVersionUID = 1297900641952417540L;
+
+                       @Override
+                       protected void setContainerLayoutData(Composite composite) {
+                               composite.setLayoutData(CmsUtils.grabWidth(SWT.CENTER,
+                                               SWT.DEFAULT));
+                       }
+
+                       @Override
+                       protected void setControlLayoutData(Control control) {
+                               control.setLayoutData(CmsUtils.grabWidth(SWT.CENTER,
+                                               SWT.DEFAULT));
+                       }
+               };
+               img.setLayoutData(CmsUtils.grabWidth(SWT.CENTER, SWT.DEFAULT));
+               updateContent(img);
+               img.setMouseListener(getMouseListener());
+               return img;
+       }
+
+       protected SectionTitle newSectionTitle(TextSection parent, Node node)
+                       throws RepositoryException {
+               SectionTitle title = new SectionTitle(parent.getHeader(),
+                               parent.getStyle(), node.getProperty(JCR_TITLE));
+               updateContent(title);
+               title.setMouseListener(getMouseListener());
+               return title;
+       }
+
+       protected SectionTitle prepareSectionTitle(Section newSection,
+                       String titleText) throws RepositoryException {
+               Node sectionNode = newSection.getNode();
+               if (!sectionNode.hasProperty(JCR_TITLE))
+                       sectionNode.setProperty(Property.JCR_TITLE, "");
+               getTextInterpreter().write(sectionNode.getProperty(Property.JCR_TITLE),
+                               titleText);
+               if (newSection.getHeader() == null)
+                       newSection.createHeader();
+               SectionTitle sectionTitle = newSectionTitle((TextSection) newSection,
+                               sectionNode);
+               return sectionTitle;
+       }
+
+       protected void updateContent(EditablePart part) throws RepositoryException {
+               if (part instanceof SectionPart) {
+                       SectionPart sectionPart = (SectionPart) part;
+                       Node partNode = sectionPart.getNode();
+
+                       if (part instanceof StyledControl
+                                       && (sectionPart.getSection() instanceof TextSection)) {
+                               TextSection section = (TextSection) sectionPart.getSection();
+                               StyledControl styledControl = (StyledControl) part;
+                               if (partNode.isNodeType(CmsTypes.CMS_STYLED)) {
+                                       String style = partNode.hasProperty(CMS_STYLE) ? partNode
+                                                       .getProperty(CMS_STYLE).getString() : section
+                                                       .getDefaultTextStyle();
+                                       styledControl.setStyle(style);
+                               }
+                       }
+                       // use control AFTER setting style, since it may have been reset
+
+                       if (part instanceof EditableText) {
+                               EditableText paragraph = (EditableText) part;
+                               if (paragraph == getEdited())
+                                       paragraph.setText(textInterpreter.read(partNode));
+                               else
+                                       paragraph.setText(textInterpreter.raw(partNode));
+                       } else if (part instanceof EditableImage) {
+                               EditableImage editableImage = (EditableImage) part;
+                               imageManager.load(partNode, part.getControl(),
+                                               editableImage.getPreferredImageSize());
+                       }
+               } else if (part instanceof SectionTitle) {
+                       SectionTitle title = (SectionTitle) part;
+                       title.setStyle(title.getSection().getTitleStyle());
+                       // use control AFTER setting style
+                       if (title == getEdited())
+                               title.setText(textInterpreter.read(title.getProperty()));
+                       else
+                               title.setText(textInterpreter.raw(title.getProperty()));
+               }
+       }
+
+       // OVERRIDDEN FROM PARENT VIEWER
+       @Override
+       protected void save(EditablePart part) throws RepositoryException {
+               if (part instanceof EditableText) {
+                       EditableText et = (EditableText) part;
+                       String text = ((Text) et.getControl()).getText();
+
+                       String[] lines = text.split("[\r\n]+");
+                       assert lines.length != 0;
+                       saveLine(part, lines[0]);
+                       if (lines.length > 1) {
+                               ArrayList<Control> toLayout = new ArrayList<Control>();
+                               if (part instanceof Paragraph) {
+                                       Paragraph currentParagraph = (Paragraph) et;
+                                       Section section = currentParagraph.getSection();
+                                       Node sectionNode = section.getNode();
+                                       Node currentParagraphN = currentParagraph.getNode();
+                                       for (int i = 1; i < lines.length; i++) {
+                                               Node newNode = sectionNode.addNode(CMS_P);
+                                               newNode.addMixin(CmsTypes.CMS_STYLED);
+                                               saveLine(newNode, lines[i]);
+                                               // second node was create as last, if it is not the next
+                                               // one, it
+                                               // means there are some in between and we can take the
+                                               // one at
+                                               // index+1 for the re-order
+                                               if (newNode.getIndex() > currentParagraphN.getIndex() + 1) {
+                                                       sectionNode.orderBefore(p(newNode.getIndex()),
+                                                                       p(currentParagraphN.getIndex() + 1));
+                                               }
+                                               Paragraph newParagraph = newParagraph(
+                                                               (TextSection) section, newNode);
+                                               newParagraph.moveBelow(currentParagraph);
+                                               toLayout.add(newParagraph);
+
+                                               currentParagraph = newParagraph;
+                                               currentParagraphN = newNode;
+                                       }
+                                       persistChanges(sectionNode);
+                               }
+                               // TODO or rather return the created paragarphs?
+                               layout(toLayout.toArray(new Control[toLayout.size()]));
+                       }
+               }
+       }
+
+       protected void saveLine(EditablePart part, String line) {
+               if (part instanceof NodePart) {
+                       saveLine(((NodePart) part).getNode(), line);
+               } else if (part instanceof PropertyPart) {
+                       saveLine(((PropertyPart) part).getProperty(), line);
+               } else {
+                       throw new CmsException("Unsupported part " + part);
+               }
+       }
+
+       protected void saveLine(Item item, String line) {
+               line = line.trim();
+               textInterpreter.write(item, line);
+       }
+
+       @Override
+       protected void prepare(EditablePart part, Object caretPosition) {
+               Control control = part.getControl();
+               if (control instanceof Text) {
+                       Text text = (Text) control;
+                       if (caretPosition != null)
+                               if (caretPosition instanceof Integer)
+                                       text.setSelection((Integer) caretPosition);
+                               else if (caretPosition instanceof Point) {
+                                       // TODO find a way to position the caret at the right place
+                               }
+                       text.setData(RWT.ACTIVE_KEYS, new String[] { "BACKSPACE", "ESC",
+                                       "TAB", "SHIFT+TAB", "ALT+ARROW_LEFT", "ALT+ARROW_RIGHT",
+                                       "ALT+ARROW_UP", "ALT+ARROW_DOWN", "RETURN", "CTRL+RETURN",
+                                       "ENTER", "DELETE" });
+                       text.setData(RWT.CANCEL_KEYS, new String[] { "RETURN",
+                                       "ALT+ARROW_LEFT", "ALT+ARROW_RIGHT" });
+                       text.addKeyListener(this);
+               } else if (part instanceof Img) {
+                       ((Img) part).setFileUploadListener(fileUploadListener);
+               }
+       }
+
+       // REQUIRED BY CONTEXT MENU
+       void setParagraphStyle(Paragraph paragraph, String style) {
+               try {
+                       Node paragraphNode = paragraph.getNode();
+                       paragraphNode.setProperty(CMS_STYLE, style);
+                       persistChanges(paragraphNode);
+                       updateContent(paragraph);
+                       layout(paragraph);
+               } catch (RepositoryException e1) {
+                       throw new CmsException("Cannot set style " + style + " on "
+                                       + paragraph, e1);
+               }
+       }
+
+       void deletePart(SectionPart paragraph) {
+               try {
+                       Node paragraphNode = paragraph.getNode();
+                       Section section = paragraph.getSection();
+                       Session session = paragraphNode.getSession();
+                       paragraphNode.remove();
+                       session.save();
+                       if (paragraph instanceof Control)
+                               ((Control) paragraph).dispose();
+                       layout(section);
+               } catch (RepositoryException e1) {
+                       throw new CmsException("Cannot delete " + paragraph, e1);
+               }
+       }
+
+       String getRawParagraphText(Paragraph paragraph) {
+               return textInterpreter.raw(paragraph.getNode());
+       }
+
+       // COMMANDS
+       protected void splitEdit() {
+               checkEdited();
+               try {
+                       if (getEdited() instanceof Paragraph) {
+                               Paragraph paragraph = (Paragraph) getEdited();
+                               Text text = (Text) paragraph.getControl();
+                               int caretPosition = text.getCaretPosition();
+                               String txt = text.getText();
+                               String first = txt.substring(0, caretPosition);
+                               String second = txt.substring(caretPosition);
+                               Node firstNode = paragraph.getNode();
+                               Node sectionNode = firstNode.getParent();
+                               firstNode.setProperty(CMS_CONTENT, first);
+                               Node secondNode = sectionNode.addNode(CMS_P);
+                               secondNode.addMixin(CmsTypes.CMS_STYLED);
+                               // second node was create as last, if it is not the next one, it
+                               // means there are some in between and we can take the one at
+                               // index+1 for the re-order
+                               if (secondNode.getIndex() > firstNode.getIndex() + 1) {
+                                       sectionNode.orderBefore(p(secondNode.getIndex()),
+                                                       p(firstNode.getIndex() + 1));
+                               }
+
+                               // if we die in between, at least we still have the whole text
+                               // in the first node
+                               try {
+                                       textInterpreter.write(secondNode, second);
+                                       textInterpreter.write(firstNode, first);
+                               } catch (Exception e) {
+                                       // so that no additional nodes are created:
+                                       JcrUtils.discardUnderlyingSessionQuietly(firstNode);
+                                       throw e;
+                               }
+
+                               persistChanges(firstNode);
+
+                               Paragraph secondParagraph = paragraphSplitted(paragraph,
+                                               secondNode);
+                               edit(secondParagraph, 0);
+                       } else if (getEdited() instanceof SectionTitle) {
+                               SectionTitle sectionTitle = (SectionTitle) getEdited();
+                               Text text = (Text) sectionTitle.getControl();
+                               String txt = text.getText();
+                               int caretPosition = text.getCaretPosition();
+                               Section section = sectionTitle.getSection();
+                               Node sectionNode = section.getNode();
+                               Node paragraphNode = sectionNode.addNode(CMS_P);
+                               paragraphNode.addMixin(CmsTypes.CMS_STYLED);
+                               textInterpreter.write(paragraphNode,
+                                               txt.substring(caretPosition));
+                               textInterpreter.write(
+                                               sectionNode.getProperty(Property.JCR_TITLE),
+                                               txt.substring(0, caretPosition));
+                               sectionNode.orderBefore(p(paragraphNode.getIndex()), p(1));
+                               persistChanges(sectionNode);
+
+                               Paragraph paragraph = sectionTitleSplitted(sectionTitle,
+                                               paragraphNode);
+                               // section.layout();
+                               edit(paragraph, 0);
+                       }
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot split " + getEdited(), e);
+               }
+       }
+
+       protected void mergeWithPrevious() {
+               checkEdited();
+               try {
+                       Paragraph paragraph = (Paragraph) getEdited();
+                       Text text = (Text) paragraph.getControl();
+                       String txt = text.getText();
+                       Node paragraphNode = paragraph.getNode();
+                       if (paragraphNode.getIndex() == 1)
+                               return;// do nothing
+                       Node sectionNode = paragraphNode.getParent();
+                       Node previousNode = sectionNode
+                                       .getNode(p(paragraphNode.getIndex() - 1));
+                       String previousTxt = textInterpreter.read(previousNode);
+                       textInterpreter.write(previousNode, previousTxt + txt);
+                       paragraphNode.remove();
+                       persistChanges(sectionNode);
+
+                       Paragraph previousParagraph = paragraphMergedWithPrevious(
+                                       paragraph, previousNode);
+                       edit(previousParagraph, previousTxt.length());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot stop editing", e);
+               }
+       }
+
+       protected void mergeWithNext() {
+               checkEdited();
+               try {
+                       Paragraph paragraph = (Paragraph) getEdited();
+                       Text text = (Text) paragraph.getControl();
+                       String txt = text.getText();
+                       Node paragraphNode = paragraph.getNode();
+                       Node sectionNode = paragraphNode.getParent();
+                       NodeIterator paragraphNodes = sectionNode.getNodes(CMS_P);
+                       long size = paragraphNodes.getSize();
+                       if (paragraphNode.getIndex() == size)
+                               return;// do nothing
+                       Node nextNode = sectionNode
+                                       .getNode(p(paragraphNode.getIndex() + 1));
+                       String nextTxt = textInterpreter.read(nextNode);
+                       textInterpreter.write(paragraphNode, txt + nextTxt);
+
+                       Section section = paragraph.getSection();
+                       Paragraph removed = (Paragraph) section.getSectionPart(nextNode
+                                       .getIdentifier());
+
+                       nextNode.remove();
+                       persistChanges(sectionNode);
+
+                       paragraphMergedWithNext(paragraph, removed);
+                       edit(paragraph, txt.length());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot stop editing", e);
+               }
+       }
+
+       protected synchronized void upload(EditablePart part) {
+               try {
+                       if (part instanceof SectionPart) {
+                               SectionPart sectionPart = (SectionPart) part;
+                               Node partNode = sectionPart.getNode();
+                               int partIndex = partNode.getIndex();
+                               Section section = sectionPart.getSection();
+                               Node sectionNode = section.getNode();
+
+                               if (part instanceof Paragraph) {
+                                       Node newNode = sectionNode.addNode(CMS_P, NodeType.NT_FILE);
+                                       newNode.addNode(Node.JCR_CONTENT, NodeType.NT_RESOURCE);
+                                       JcrUtils.copyBytesAsFile(sectionNode,
+                                                       p(newNode.getIndex()), new byte[0]);
+                                       if (partIndex < newNode.getIndex() - 1) {
+                                               // was not last
+                                               sectionNode.orderBefore(p(newNode.getIndex()),
+                                                               p(partIndex - 1));
+                                       }
+                                       // sectionNode.orderBefore(p(partNode.getIndex()),
+                                       // p(newNode.getIndex()));
+                                       persistChanges(sectionNode);
+                                       Img img = newImg((TextSection) section, newNode);
+                                       edit(img, null);
+                                       layout(img.getControl());
+                               } else if (part instanceof Img) {
+                                       if (getEdited() == part)
+                                               return;
+                                       edit(part, null);
+                                       layout(part.getControl());
+                               }
+                       }
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot upload", e);
+               }
+       }
+
+       protected void deepen() {
+               if (flat)
+                       return;
+               checkEdited();
+               try {
+                       if (getEdited() instanceof Paragraph) {
+                               Paragraph paragraph = (Paragraph) getEdited();
+                               Text text = (Text) paragraph.getControl();
+                               String txt = text.getText();
+                               Node paragraphNode = paragraph.getNode();
+                               Section section = paragraph.getSection();
+                               Node sectionNode = section.getNode();
+                               // main title
+                               if (section == mainSection && section instanceof TextSection
+                                               && paragraphNode.getIndex() == 1
+                                               && !sectionNode.hasProperty(JCR_TITLE)) {
+                                       SectionTitle sectionTitle = prepareSectionTitle(section,
+                                                       txt);
+                                       edit(sectionTitle, 0);
+                                       return;
+                               }
+                               Node newSectionNode = sectionNode.addNode(CMS_H,
+                                               CmsTypes.CMS_SECTION);
+                               sectionNode.orderBefore(h(newSectionNode.getIndex()), h(1));
+
+                               int paragraphIndex = paragraphNode.getIndex();
+                               String sectionPath = sectionNode.getPath();
+                               String newSectionPath = newSectionNode.getPath();
+                               while (sectionNode.hasNode(p(paragraphIndex + 1))) {
+                                       Node parag = sectionNode.getNode(p(paragraphIndex + 1));
+                                       sectionNode.getSession().move(
+                                                       sectionPath + '/' + p(paragraphIndex + 1),
+                                                       newSectionPath + '/' + CMS_P);
+                                       SectionPart sp = section.getSectionPart(parag
+                                                       .getIdentifier());
+                                       if (sp instanceof Control)
+                                               ((Control) sp).dispose();
+                               }
+                               // create property
+                               newSectionNode.setProperty(Property.JCR_TITLE, "");
+                               getTextInterpreter().write(
+                                               newSectionNode.getProperty(Property.JCR_TITLE), txt);
+
+                               TextSection newSection = new TextSection(section,
+                                               section.getStyle(), newSectionNode);
+                               newSection.setLayoutData(CmsUtils.fillWidth());
+                               newSection.moveBelow(paragraph);
+
+                               // dispose
+                               paragraphNode.remove();
+                               paragraph.dispose();
+
+                               refresh(newSection);
+                               newSection.getParent().layout();
+                               layout(newSection);
+                               persistChanges(sectionNode);
+                       } else if (getEdited() instanceof SectionTitle) {
+                               SectionTitle sectionTitle = (SectionTitle) getEdited();
+                               Section section = sectionTitle.getSection();
+                               Section parentSection = section.getParentSection();
+                               if (parentSection == null)
+                                       return;// cannot deepen main section
+                               Node sectionN = section.getNode();
+                               Node parentSectionN = parentSection.getNode();
+                               if (sectionN.getIndex() == 1)
+                                       return;// cannot deepen first section
+                               Node previousSectionN = parentSectionN.getNode(h(sectionN
+                                               .getIndex() - 1));
+                               NodeIterator subSections = previousSectionN.getNodes(CMS_H);
+                               int subsectionsCount = (int) subSections.getSize();
+                               previousSectionN.getSession().move(
+                                               sectionN.getPath(),
+                                               previousSectionN.getPath() + "/"
+                                                               + h(subsectionsCount + 1));
+                               section.dispose();
+                               TextSection newSection = new TextSection(section,
+                                               section.getStyle(), sectionN);
+                               refresh(newSection);
+                               persistChanges(previousSectionN);
+                       }
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot deepen " + getEdited(), e);
+               }
+       }
+
+       protected void undeepen() {
+               if (flat)
+                       return;
+               checkEdited();
+               try {
+                       if (getEdited() instanceof Paragraph) {
+                               upload(getEdited());
+                       } else if (getEdited() instanceof SectionTitle) {
+                               SectionTitle sectionTitle = (SectionTitle) getEdited();
+                               Section section = sectionTitle.getSection();
+                               Node sectionNode = section.getNode();
+                               Section parentSection = section.getParentSection();
+                               if (parentSection == null)
+                                       return;// cannot undeepen main section
+
+                               // choose in which section to merge
+                               Section mergedSection;
+                               if (sectionNode.getIndex() == 1)
+                                       mergedSection = section.getParentSection();
+                               else {
+                                       Map<String, Section> parentSubsections = parentSection
+                                                       .getSubSections();
+                                       ArrayList<Section> lst = new ArrayList<Section>(
+                                                       parentSubsections.values());
+                                       mergedSection = lst.get(sectionNode.getIndex() - 1);
+                               }
+                               Node mergedNode = mergedSection.getNode();
+                               boolean mergedHasSubSections = mergedNode.hasNode(CMS_H);
+
+                               // title as paragraph
+                               Node newParagrapheNode = mergedNode.addNode(CMS_P);
+                               newParagrapheNode.addMixin(CmsTypes.CMS_STYLED);
+                               if (mergedHasSubSections)
+                                       mergedNode.orderBefore(p(newParagrapheNode.getIndex()),
+                                                       h(1));
+                               String txt = getTextInterpreter().read(
+                                               sectionNode.getProperty(Property.JCR_TITLE));
+                               getTextInterpreter().write(newParagrapheNode, txt);
+                               // move
+                               NodeIterator paragraphs = sectionNode.getNodes(CMS_P);
+                               while (paragraphs.hasNext()) {
+                                       Node p = paragraphs.nextNode();
+                                       SectionPart sp = section.getSectionPart(p.getIdentifier());
+                                       if (sp instanceof Control)
+                                               ((Control) sp).dispose();
+                                       mergedNode.getSession().move(p.getPath(),
+                                                       mergedNode.getPath() + '/' + CMS_P);
+                                       if (mergedHasSubSections)
+                                               mergedNode.orderBefore(p(p.getIndex()), h(1));
+                               }
+
+                               Iterator<Section> subsections = section.getSubSections()
+                                               .values().iterator();
+                               // NodeIterator sections = sectionNode.getNodes(CMS_H);
+                               while (subsections.hasNext()) {
+                                       Section subsection = subsections.next();
+                                       Node s = subsection.getNode();
+                                       mergedNode.getSession().move(s.getPath(),
+                                                       mergedNode.getPath() + '/' + CMS_H);
+                                       subsection.dispose();
+                               }
+
+                               // remove section
+                               section.getNode().remove();
+                               section.dispose();
+
+                               refresh(mergedSection);
+                               mergedSection.getParent().layout();
+                               layout(mergedSection);
+                               persistChanges(mergedNode);
+                       }
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot undeepen " + getEdited(), e);
+               }
+       }
+
+       // UI CHANGES
+       protected Paragraph paragraphSplitted(Paragraph paragraph, Node newNode)
+                       throws RepositoryException {
+               Section section = paragraph.getSection();
+               updateContent(paragraph);
+               Paragraph newParagraph = newParagraph((TextSection) section, newNode);
+               newParagraph.setLayoutData(CmsUtils.fillWidth());
+               newParagraph.moveBelow(paragraph);
+               layout(paragraph.getControl(), newParagraph.getControl());
+               return newParagraph;
+       }
+
+       protected Paragraph sectionTitleSplitted(SectionTitle sectionTitle,
+                       Node newNode) throws RepositoryException {
+               updateContent(sectionTitle);
+               Paragraph newParagraph = newParagraph(sectionTitle.getSection(),
+                               newNode);
+               // we assume beforeFirst is not null since there was a sectionTitle
+               newParagraph.moveBelow(sectionTitle.getSection().getHeader());
+               layout(sectionTitle.getControl(), newParagraph.getControl());
+               return newParagraph;
+       }
+
+       protected Paragraph paragraphMergedWithPrevious(Paragraph removed,
+                       Node remaining) throws RepositoryException {
+               Section section = removed.getSection();
+               removed.dispose();
+
+               Paragraph paragraph = (Paragraph) section.getSectionPart(remaining
+                               .getIdentifier());
+               updateContent(paragraph);
+               layout(paragraph.getControl());
+               return paragraph;
+       }
+
+       protected void paragraphMergedWithNext(Paragraph remaining,
+                       Paragraph removed) throws RepositoryException {
+               removed.dispose();
+               updateContent(remaining);
+               layout(remaining.getControl());
+       }
+
+       // UTILITIES
+       protected String p(Integer index) {
+               StringBuilder sb = new StringBuilder(6);
+               sb.append(CMS_P).append('[').append(index).append(']');
+               return sb.toString();
+       }
+
+       protected String h(Integer index) {
+               StringBuilder sb = new StringBuilder(5);
+               sb.append(CMS_H).append('[').append(index).append(']');
+               return sb.toString();
+       }
+
+       // GETTERS / SETTERS
+       public Section getMainSection() {
+               return mainSection;
+       }
+
+       public boolean isFlat() {
+               return flat;
+       }
+
+       public TextInterpreter getTextInterpreter() {
+               return textInterpreter;
+       }
+
+       // KEY LISTENER
+       @Override
+       public void keyPressed(KeyEvent ke) {
+               if (log.isTraceEnabled())
+                       log.trace(ke);
+
+               if (getEdited() == null)
+                       return;
+               boolean altPressed = (ke.stateMask & SWT.ALT) != 0;
+               boolean shiftPressed = (ke.stateMask & SWT.SHIFT) != 0;
+               boolean ctrlPressed = (ke.stateMask & SWT.CTRL) != 0;
+
+               try {
+                       // Common
+                       if (ke.keyCode == SWT.ESC) {
+                               cancelEdit();
+                       } else if (ke.character == '\r') {
+                               splitEdit();
+                       } else if (ke.character == 'S') {
+                               if (ctrlPressed)
+                                       saveEdit();
+                       } else if (ke.character == '\t') {
+                               if (!shiftPressed) {
+                                       deepen();
+                               } else if (shiftPressed) {
+                                       undeepen();
+                               }
+                       } else {
+                               if (getEdited() instanceof Paragraph) {
+                                       Paragraph paragraph = (Paragraph) getEdited();
+                                       Section section = paragraph.getSection();
+                                       if (altPressed && ke.keyCode == SWT.ARROW_RIGHT) {
+                                               edit(section.nextSectionPart(paragraph), 0);
+                                       } else if (altPressed && ke.keyCode == SWT.ARROW_LEFT) {
+                                               edit(section.previousSectionPart(paragraph), 0);
+                                       } else if (ke.character == SWT.BS) {
+                                               Text text = (Text) paragraph.getControl();
+                                               int caretPosition = text.getCaretPosition();
+                                               if (caretPosition == 0) {
+                                                       mergeWithPrevious();
+                                               }
+                                       } else if (ke.character == SWT.DEL) {
+                                               Text text = (Text) paragraph.getControl();
+                                               int caretPosition = text.getCaretPosition();
+                                               int charcount = text.getCharCount();
+                                               if (caretPosition == charcount) {
+                                                       mergeWithNext();
+                                               }
+                                       }
+                               }
+                       }
+               } catch (Exception e) {
+                       ke.doit = false;
+                       notifyEditionException(e);
+               }
+       }
+
+       @Override
+       public void keyReleased(KeyEvent e) {
+       }
+
+       // MOUSE LISTENER
+       @Override
+       protected MouseListener createMouseListener() {
+               return new ML();
+       }
+
+       private class ML extends MouseAdapter {
+               private static final long serialVersionUID = 8526890859876770905L;
+
+               @Override
+               public void mouseDoubleClick(MouseEvent e) {
+                       if (e.button == 1) {
+                               Control source = (Control) e.getSource();
+                               if (getCmsEditable().canEdit()) {
+                                       if (getCmsEditable().isEditing()
+                                                       && !(getEdited() instanceof Img)) {
+                                               if (source == mainSection)
+                                                       return;
+                                               EditablePart part = findDataParent(source);
+                                               upload(part);
+                                       } else {
+                                               getCmsEditable().startEditing();
+                                       }
+                               }
+                       }
+               }
+
+               @Override
+               public void mouseDown(MouseEvent e) {
+                       if (getCmsEditable().isEditing()) {
+                               if (e.button == 1) {
+                                       Control source = (Control) e.getSource();
+                                       EditablePart composite = findDataParent(source);
+                                       Point point = new Point(e.x, e.y);
+                                       if (!(composite instanceof Img))
+                                               edit(composite, source.toDisplay(point));
+                               } else if (e.button == 3) {
+                                       EditablePart composite = findDataParent((Control) e
+                                                       .getSource());
+                                       if (styledTools != null)
+                                               styledTools.show(composite, new Point(e.x, e.y));
+                               }
+                       }
+               }
+
+               @Override
+               public void mouseUp(MouseEvent e) {
+               }
+       }
+
+       // FILE UPLOAD LISTENER
+       private class FUL implements FileUploadListener {
+               public void uploadProgress(FileUploadEvent event) {
+                       // TODO Monitor upload progress
+               }
+
+               public void uploadFailed(FileUploadEvent event) {
+                       throw new CmsException("Upload failed " + event,
+                                       event.getException());
+               }
+
+               public void uploadFinished(FileUploadEvent event) {
+                       for (FileDetails file : event.getFileDetails()) {
+                               if (log.isDebugEnabled())
+                                       log.debug("Received: " + file.getFileName());
+                       }
+                       mainSection.getDisplay().syncExec(new Runnable() {
+                               @Override
+                               public void run() {
+                                       saveEdit();
+                               }
+                       });
+                       FileUploadHandler uploadHandler = (FileUploadHandler) event
+                                       .getSource();
+                       uploadHandler.dispose();
+               }
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/DbkContextMenu.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/DbkContextMenu.java
new file mode 100644 (file)
index 0000000..181a584
--- /dev/null
@@ -0,0 +1,135 @@
+package org.argeo.cms.ui.internal.text;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.text.Paragraph;
+import org.argeo.cms.text.TextStyles;
+import org.argeo.cms.viewers.EditablePart;
+import org.argeo.cms.viewers.SectionPart;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/** Dialog to edit a text part. */
+class DbkContextMenu extends Shell implements CmsNames, TextStyles {
+       private final static String[] DEFAULT_TEXT_STYLES = {
+                       TextStyles.TEXT_DEFAULT, TextStyles.TEXT_PRE, TextStyles.TEXT_QUOTE };
+
+       private final AbstractDbkViewer textViewer;
+
+       private static final long serialVersionUID = -3826246895162050331L;
+       private List<StyleButton> styleButtons = new ArrayList<DbkContextMenu.StyleButton>();
+
+       private Label deleteButton, publishButton, editButton;
+
+       private EditablePart currentTextPart;
+
+       public DbkContextMenu(AbstractDbkViewer textViewer, Display display) {
+               super(display, SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+               this.textViewer = textViewer;
+               setLayout(new GridLayout());
+               setData(RWT.CUSTOM_VARIANT, TEXT_STYLED_TOOLS_DIALOG);
+
+               StyledToolMouseListener stml = new StyledToolMouseListener();
+               if (textViewer.getCmsEditable().isEditing()) {
+                       for (String style : DEFAULT_TEXT_STYLES) {
+                               StyleButton styleButton = new StyleButton(this, SWT.WRAP);
+                               styleButton.setData(RWT.CUSTOM_VARIANT, style);
+                               styleButton.setData(RWT.MARKUP_ENABLED, true);
+                               styleButton.addMouseListener(stml);
+                               styleButtons.add(styleButton);
+                       }
+
+                       // Delete
+                       deleteButton = new Label(this, SWT.NONE);
+                       deleteButton.setText("Delete");
+                       deleteButton.addMouseListener(stml);
+
+                       // Publish
+                       publishButton = new Label(this, SWT.NONE);
+                       publishButton.setText("Publish");
+                       publishButton.addMouseListener(stml);
+               } else if (textViewer.getCmsEditable().canEdit()) {
+                       // Edit
+                       editButton = new Label(this, SWT.NONE);
+                       editButton.setText("Edit");
+                       editButton.addMouseListener(stml);
+               }
+               addShellListener(new ToolsShellListener());
+       }
+
+       public void show(EditablePart source, Point location) {
+               if (isVisible())
+                       setVisible(false);
+
+               this.currentTextPart = source;
+
+               if (currentTextPart instanceof Paragraph) {
+                       final int size = 32;
+                       String text = textViewer
+                                       .getRawParagraphText((Paragraph) currentTextPart);
+                       String textToShow = text.length() > size ? text.substring(0,
+                                       size - 3) + "..." : text;
+                       for (StyleButton styleButton : styleButtons) {
+                               styleButton.setText(textToShow);
+                       }
+               }
+               pack();
+               layout();
+               if (source instanceof Control)
+                       setLocation(((Control) source).toDisplay(location.x, location.y));
+               open();
+       }
+
+       class StyleButton extends Label {
+               private static final long serialVersionUID = 7731102609123946115L;
+
+               public StyleButton(Composite parent, int swtStyle) {
+                       super(parent, swtStyle);
+               }
+
+       }
+
+       class StyledToolMouseListener extends MouseAdapter {
+               private static final long serialVersionUID = 8516297091549329043L;
+
+               @Override
+               public void mouseDown(MouseEvent e) {
+                       Object eventSource = e.getSource();
+                       if (eventSource instanceof StyleButton) {
+                               StyleButton sb = (StyleButton) e.getSource();
+                               String style = sb.getData(RWT.CUSTOM_VARIANT).toString();
+                               textViewer
+                                               .setParagraphStyle((Paragraph) currentTextPart, style);
+                       } else if (eventSource == deleteButton) {
+                               textViewer.deletePart((SectionPart) currentTextPart);
+                       } else if (eventSource == editButton) {
+                               textViewer.getCmsEditable().startEditing();
+                       } else if (eventSource == publishButton) {
+                               textViewer.getCmsEditable().stopEditing();
+                       }
+                       setVisible(false);
+               }
+       }
+
+       class ToolsShellListener extends org.eclipse.swt.events.ShellAdapter {
+               private static final long serialVersionUID = 8432350564023247241L;
+
+               @Override
+               public void shellDeactivated(ShellEvent e) {
+                       setVisible(false);
+               }
+
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/MarkupValidatorCopy.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/MarkupValidatorCopy.java
new file mode 100644 (file)
index 0000000..9bced0d
--- /dev/null
@@ -0,0 +1,184 @@
+package org.argeo.cms.ui.internal.text;
+
+import java.io.StringReader;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.argeo.cms.forms.FormPageViewer;
+import org.eclipse.rap.rwt.SingletonUtil;
+import org.eclipse.swt.widgets.Widget;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Copy of RAP v2.3 since it is in an internal package.
+ * 
+ * FIXME made public to enable validation from the {@link FormPageViewer}
+ */
+public class MarkupValidatorCopy {
+
+       // Used by Eclipse Scout project
+       public static final String MARKUP_VALIDATION_DISABLED = "org.eclipse.rap.rwt.markupValidationDisabled";
+
+       private static final String DTD = createDTD();
+       private static final Map<String, String[]> SUPPORTED_ELEMENTS = createSupportedElementsMap();
+       private final SAXParser saxParser;
+
+       public static MarkupValidatorCopy getInstance() {
+               return SingletonUtil.getSessionInstance(MarkupValidatorCopy.class);
+       }
+
+       public MarkupValidatorCopy() {
+               saxParser = createSAXParser();
+       }
+
+       public void validate(String text) {
+               StringBuilder markup = new StringBuilder();
+               markup.append(DTD);
+               markup.append("<html>");
+               markup.append(text);
+               markup.append("</html>");
+               InputSource inputSource = new InputSource(new StringReader(
+                               markup.toString()));
+               try {
+                       saxParser.parse(inputSource, new MarkupHandler());
+               } catch (RuntimeException exception) {
+                       throw exception;
+               } catch (Exception exception) {
+                       throw new IllegalArgumentException("Failed to parse markup text",
+                                       exception);
+               }
+       }
+
+       public static boolean isValidationDisabledFor(Widget widget) {
+               return Boolean.TRUE.equals(widget.getData(MARKUP_VALIDATION_DISABLED));
+       }
+
+       private static SAXParser createSAXParser() {
+               SAXParser result = null;
+               SAXParserFactory parserFactory = SAXParserFactory.newInstance();
+               try {
+                       result = parserFactory.newSAXParser();
+               } catch (Exception exception) {
+                       throw new RuntimeException("Failed to create SAX parser", exception);
+               }
+               return result;
+       }
+
+       private static String createDTD() {
+               StringBuilder result = new StringBuilder();
+               result.append("<!DOCTYPE html [");
+               result.append("<!ENTITY quot \"&#34;\">");
+               result.append("<!ENTITY amp \"&#38;\">");
+               result.append("<!ENTITY apos \"&#39;\">");
+               result.append("<!ENTITY lt \"&#60;\">");
+               result.append("<!ENTITY gt \"&#62;\">");
+               result.append("<!ENTITY nbsp \"&#160;\">");
+               result.append("<!ENTITY ensp \"&#8194;\">");
+               result.append("<!ENTITY emsp \"&#8195;\">");
+               result.append("<!ENTITY ndash \"&#8211;\">");
+               result.append("<!ENTITY mdash \"&#8212;\">");
+               result.append("]>");
+               return result.toString();
+       }
+
+       private static Map<String, String[]> createSupportedElementsMap() {
+               Map<String, String[]> result = new HashMap<String, String[]>();
+               result.put("html", new String[0]);
+               result.put("br", new String[0]);
+               result.put("b", new String[] { "style" });
+               result.put("strong", new String[] { "style" });
+               result.put("i", new String[] { "style" });
+               result.put("em", new String[] { "style" });
+               result.put("sub", new String[] { "style" });
+               result.put("sup", new String[] { "style" });
+               result.put("big", new String[] { "style" });
+               result.put("small", new String[] { "style" });
+               result.put("del", new String[] { "style" });
+               result.put("ins", new String[] { "style" });
+               result.put("code", new String[] { "style" });
+               result.put("samp", new String[] { "style" });
+               result.put("kbd", new String[] { "style" });
+               result.put("var", new String[] { "style" });
+               result.put("cite", new String[] { "style" });
+               result.put("dfn", new String[] { "style" });
+               result.put("q", new String[] { "style" });
+               result.put("abbr", new String[] { "style", "title" });
+               result.put("span", new String[] { "style" });
+               result.put("img", new String[] { "style", "src", "width", "height",
+                               "title", "alt" });
+               result.put("a", new String[] { "style", "href", "target", "title" });
+               return result;
+       }
+
+       private static class MarkupHandler extends DefaultHandler {
+
+               @Override
+               public void startElement(String uri, String localName, String name,
+                               Attributes attributes) {
+                       checkSupportedElements(name, attributes);
+                       checkSupportedAttributes(name, attributes);
+                       checkMandatoryAttributes(name, attributes);
+               }
+
+               private static void checkSupportedElements(String elementName,
+                               Attributes attributes) {
+                       if (!SUPPORTED_ELEMENTS.containsKey(elementName)) {
+                               throw new IllegalArgumentException(
+                                               "Unsupported element in markup text: " + elementName);
+                       }
+               }
+
+               private static void checkSupportedAttributes(String elementName,
+                               Attributes attributes) {
+                       if (attributes.getLength() > 0) {
+                               List<String> supportedAttributes = Arrays
+                                               .asList(SUPPORTED_ELEMENTS.get(elementName));
+                               int index = 0;
+                               String attributeName = attributes.getQName(index);
+                               while (attributeName != null) {
+                                       if (!supportedAttributes.contains(attributeName)) {
+                                               String message = "Unsupported attribute \"{0}\" for element \"{1}\" in markup text";
+                                               message = MessageFormat.format(message, new Object[] {
+                                                               attributeName, elementName });
+                                               throw new IllegalArgumentException(message);
+                                       }
+                                       index++;
+                                       attributeName = attributes.getQName(index);
+                               }
+                       }
+               }
+
+               private static void checkMandatoryAttributes(String elementName,
+                               Attributes attributes) {
+                       checkIntAttribute(elementName, attributes, "img", "width");
+                       checkIntAttribute(elementName, attributes, "img", "height");
+               }
+
+               private static void checkIntAttribute(String elementName,
+                               Attributes attributes, String checkedElementName,
+                               String checkedAttributeName) {
+                       if (checkedElementName.equals(elementName)) {
+                               String attribute = attributes.getValue(checkedAttributeName);
+                               try {
+                                       Integer.parseInt(attribute);
+                               } catch (NumberFormatException exception) {
+                                       String message = "Mandatory attribute \"{0}\" for element \"{1}\" is missing or not a valid integer";
+                                       Object[] arguments = new Object[] { checkedAttributeName,
+                                                       checkedElementName };
+                                       message = MessageFormat.format(message, arguments);
+                                       throw new IllegalArgumentException(message);
+                               }
+                       }
+               }
+
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/SectionTitle.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/SectionTitle.java
new file mode 100644 (file)
index 0000000..24861ee
--- /dev/null
@@ -0,0 +1,39 @@
+package org.argeo.cms.ui.internal.text;
+
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.text.TextSection;
+import org.argeo.cms.viewers.EditablePart;
+import org.argeo.cms.viewers.PropertyPart;
+import org.argeo.cms.widgets.EditableText;
+import org.eclipse.swt.widgets.Composite;
+
+/** The title of a section. */
+public class SectionTitle extends EditableText implements EditablePart,
+               PropertyPart {
+       private static final long serialVersionUID = -1787983154946583171L;
+
+       private final TextSection section;
+
+       public SectionTitle(Composite parent, int swtStyle, Property title)
+                       throws RepositoryException {
+               super(parent, swtStyle, title);
+               section = (TextSection) TextSection.findSection(this);
+       }
+
+       public TextSection getSection() {
+               return section;
+       }
+
+       // @Override
+       // public Property getProperty() throws RepositoryException {
+       // return getSection().getNode().getProperty(Property.JCR_TITLE);
+       // }
+
+       @Override
+       public Property getItem() throws RepositoryException {
+               return getProperty();
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/TextContextMenu.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/TextContextMenu.java
new file mode 100644 (file)
index 0000000..4868b76
--- /dev/null
@@ -0,0 +1,135 @@
+package org.argeo.cms.ui.internal.text;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.text.Paragraph;
+import org.argeo.cms.text.TextStyles;
+import org.argeo.cms.viewers.EditablePart;
+import org.argeo.cms.viewers.SectionPart;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/** Dialog to edit a text part. */
+class TextContextMenu extends Shell implements CmsNames, TextStyles {
+       private final static String[] DEFAULT_TEXT_STYLES = {
+                       TextStyles.TEXT_DEFAULT, TextStyles.TEXT_PRE, TextStyles.TEXT_QUOTE };
+
+       private final AbstractTextViewer textViewer;
+
+       private static final long serialVersionUID = -3826246895162050331L;
+       private List<StyleButton> styleButtons = new ArrayList<TextContextMenu.StyleButton>();
+
+       private Label deleteButton, publishButton, editButton;
+
+       private EditablePart currentTextPart;
+
+       public TextContextMenu(AbstractTextViewer textViewer, Display display) {
+               super(display, SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+               this.textViewer = textViewer;
+               setLayout(new GridLayout());
+               setData(RWT.CUSTOM_VARIANT, TEXT_STYLED_TOOLS_DIALOG);
+
+               StyledToolMouseListener stml = new StyledToolMouseListener();
+               if (textViewer.getCmsEditable().isEditing()) {
+                       for (String style : DEFAULT_TEXT_STYLES) {
+                               StyleButton styleButton = new StyleButton(this, SWT.WRAP);
+                               styleButton.setData(RWT.CUSTOM_VARIANT, style);
+                               styleButton.setData(RWT.MARKUP_ENABLED, true);
+                               styleButton.addMouseListener(stml);
+                               styleButtons.add(styleButton);
+                       }
+
+                       // Delete
+                       deleteButton = new Label(this, SWT.NONE);
+                       deleteButton.setText("Delete");
+                       deleteButton.addMouseListener(stml);
+
+                       // Publish
+                       publishButton = new Label(this, SWT.NONE);
+                       publishButton.setText("Publish");
+                       publishButton.addMouseListener(stml);
+               } else if (textViewer.getCmsEditable().canEdit()) {
+                       // Edit
+                       editButton = new Label(this, SWT.NONE);
+                       editButton.setText("Edit");
+                       editButton.addMouseListener(stml);
+               }
+               addShellListener(new ToolsShellListener());
+       }
+
+       public void show(EditablePart source, Point location) {
+               if (isVisible())
+                       setVisible(false);
+
+               this.currentTextPart = source;
+
+               if (currentTextPart instanceof Paragraph) {
+                       final int size = 32;
+                       String text = textViewer
+                                       .getRawParagraphText((Paragraph) currentTextPart);
+                       String textToShow = text.length() > size ? text.substring(0,
+                                       size - 3) + "..." : text;
+                       for (StyleButton styleButton : styleButtons) {
+                               styleButton.setText(textToShow);
+                       }
+               }
+               pack();
+               layout();
+               if (source instanceof Control)
+                       setLocation(((Control) source).toDisplay(location.x, location.y));
+               open();
+       }
+
+       class StyleButton extends Label {
+               private static final long serialVersionUID = 7731102609123946115L;
+
+               public StyleButton(Composite parent, int swtStyle) {
+                       super(parent, swtStyle);
+               }
+
+       }
+
+       class StyledToolMouseListener extends MouseAdapter {
+               private static final long serialVersionUID = 8516297091549329043L;
+
+               @Override
+               public void mouseDown(MouseEvent e) {
+                       Object eventSource = e.getSource();
+                       if (eventSource instanceof StyleButton) {
+                               StyleButton sb = (StyleButton) e.getSource();
+                               String style = sb.getData(RWT.CUSTOM_VARIANT).toString();
+                               textViewer
+                                               .setParagraphStyle((Paragraph) currentTextPart, style);
+                       } else if (eventSource == deleteButton) {
+                               textViewer.deletePart((SectionPart) currentTextPart);
+                       } else if (eventSource == editButton) {
+                               textViewer.getCmsEditable().startEditing();
+                       } else if (eventSource == publishButton) {
+                               textViewer.getCmsEditable().stopEditing();
+                       }
+                       setVisible(false);
+               }
+       }
+
+       class ToolsShellListener extends org.eclipse.swt.events.ShellAdapter {
+               private static final long serialVersionUID = 8432350564023247241L;
+
+               @Override
+               public void shellDeactivated(ShellEvent e) {
+                       setVisible(false);
+               }
+
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/TextInterpreterImpl.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/internal/text/TextInterpreterImpl.java
new file mode 100644 (file)
index 0000000..4a646c3
--- /dev/null
@@ -0,0 +1,33 @@
+package org.argeo.cms.ui.internal.text;
+
+import javax.jcr.Item;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.text.IdentityTextInterpreter;
+
+/**
+ * Text interpreter that sanitise and validates before saving, and support CMS
+ * specific formatting and integration.
+ */
+class TextInterpreterImpl extends IdentityTextInterpreter {
+       private MarkupValidatorCopy markupValidator = MarkupValidatorCopy
+                       .getInstance();
+
+       @Override
+       protected void validateBeforeStoring(String raw) {
+               markupValidator.validate(raw);
+       }
+
+       @Override
+       protected String convertToStorage(Item item, String content)
+                       throws RepositoryException {
+               return super.convertToStorage(item, content);
+       }
+
+       @Override
+       protected String convertFromStorage(Item item, String content)
+                       throws RepositoryException {
+               return super.convertFromStorage(item, content);
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java
new file mode 100644 (file)
index 0000000..d5e639f
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Observable;
+import java.util.TreeMap;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.node.NodeConstants;
+
+public class DefaultRepositoryRegister extends Observable implements RepositoryRegister {
+       /** Key for a JCR repository alias */
+       private final static String CN = NodeConstants.CN;
+       /** Key for a JCR repository URI */
+       // public final static String JCR_REPOSITORY_URI = "argeo.jcr.repository.uri";
+       private final static Log log = LogFactory.getLog(DefaultRepositoryRegister.class);
+
+       /** Read only map which will be directly exposed. */
+       private Map<String, Repository> repositories = Collections.unmodifiableMap(new TreeMap<String, Repository>());
+
+       @SuppressWarnings("rawtypes")
+       public synchronized Repository getRepository(Map parameters) throws RepositoryException {
+               if (!parameters.containsKey(CN))
+                       throw new RepositoryException("Parameter " + CN + " has to be defined.");
+               String alias = parameters.get(CN).toString();
+               if (!repositories.containsKey(alias))
+                       throw new RepositoryException("No repository registered with alias " + alias);
+
+               return repositories.get(alias);
+       }
+
+       /** Access to the read-only map */
+       public synchronized Map<String, Repository> getRepositories() {
+               return repositories;
+       }
+
+       /** Registers a service, typically called when OSGi services are bound. */
+       @SuppressWarnings("rawtypes")
+       public synchronized void register(Repository repository, Map properties) {
+               String alias;
+               if (properties == null || !properties.containsKey(CN)) {
+                       log.warn("Cannot register a repository if no " + CN + " property is specified.");
+                       return;
+               }
+               alias = properties.get(CN).toString();
+               Map<String, Repository> map = new TreeMap<String, Repository>(repositories);
+               map.put(alias, repository);
+               repositories = Collections.unmodifiableMap(map);
+               setChanged();
+               notifyObservers(alias);
+       }
+
+       /** Unregisters a service, typically called when OSGi services are unbound. */
+       @SuppressWarnings("rawtypes")
+       public synchronized void unregister(Repository repository, Map properties) {
+               // TODO: also check bean name?
+               if (properties == null || !properties.containsKey(CN)) {
+                       log.warn("Cannot unregister a repository without property " + CN);
+                       return;
+               }
+
+               String alias = properties.get(CN).toString();
+               Map<String, Repository> map = new TreeMap<String, Repository>(repositories);
+               if (map.remove(alias) == null) {
+                       log.warn("No repository was registered with alias " + alias);
+                       return;
+               }
+               repositories = Collections.unmodifiableMap(map);
+               setChanged();
+               notifyObservers(alias);
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java
new file mode 100644 (file)
index 0000000..c9fdf56
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.version.Version;
+import javax.jcr.version.VersionHistory;
+import javax.jcr.version.VersionIterator;
+import javax.jcr.version.VersionManager;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Display some version information of a JCR full versionable node in a tree
+ * like structure
+ */
+public class FullVersioningTreeContentProvider implements ITreeContentProvider {
+       private static final long serialVersionUID = 8691772509491211112L;
+
+       /**
+        * Sends back the first level of the Tree. input element must be a single
+        * node object
+        */
+       public Object[] getElements(Object inputElement) {
+               try {
+                       Node rootNode = (Node) inputElement;
+                       String curPath = rootNode.getPath();
+                       VersionManager vm = rootNode.getSession().getWorkspace()
+                                       .getVersionManager();
+
+                       VersionHistory vh = vm.getVersionHistory(curPath);
+                       List<Version> result = new ArrayList<Version>();
+                       VersionIterator vi = vh.getAllLinearVersions();
+
+                       while (vi.hasNext()) {
+                               result.add(vi.nextVersion());
+                       }
+                       return result.toArray();
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException(
+                                       "Unexpected error while getting version elements", re);
+               }
+       }
+
+       public Object[] getChildren(Object parentElement) {
+               try {
+                       if (parentElement instanceof Version) {
+                               List<Node> tmp = new ArrayList<Node>();
+                               tmp.add(((Version) parentElement).getFrozenNode());
+                               return tmp.toArray();
+                       }
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Unexpected error while getting child "
+                                       + "node for version element", re);
+               }
+               return null;
+       }
+
+       public Object getParent(Object element) {
+               try {
+                       // this will not work in a simpleVersionning environment, parent is
+                       // not a node.
+                       if (element instanceof Node
+                                       && ((Node) element).isNodeType(NodeType.NT_FROZEN_NODE)) {
+                               Node node = (Node) element;
+                               return node.getParent();
+                       } else
+                               return null;
+               } catch (RepositoryException e) {
+                       return null;
+               }
+       }
+
+       public boolean hasChildren(Object element) {
+               try {
+                       if (element instanceof Version)
+                               return true;
+                       else if (element instanceof Node)
+                               return ((Node) element).hasNodes();
+                       else
+                               return false;
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot check children of " + element, e);
+               }
+       }
+
+       public void dispose() {
+       }
+
+       public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java
new file mode 100644 (file)
index 0000000..b19646d
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.jcr.model.RepositoriesElem;
+import org.argeo.cms.ui.jcr.model.RepositoryElem;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+
+/** Useful methods to manage the JCR Browser */
+public class JcrBrowserUtils {
+
+       public static String getPropertyTypeAsString(Property prop) {
+               try {
+                       return PropertyType.nameFromValue(prop.getType());
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot check type for " + prop, e);
+               }
+       }
+
+       /** Insure that the UI component is not stale, refresh if needed */
+       public static void forceRefreshIfNeeded(TreeParent element) {
+               Node curNode = null;
+
+               boolean doRefresh = false;
+
+               try {
+                       if (element instanceof SingleJcrNodeElem) {
+                               curNode = ((SingleJcrNodeElem) element).getNode();
+                       } else if (element instanceof WorkspaceElem) {
+                               curNode = ((WorkspaceElem) element).getRootNode();
+                       }
+
+                       if (curNode != null && element.getChildren().length != curNode.getNodes().getSize())
+                               doRefresh = true;
+                       else if (element instanceof RepositoryElem) {
+                               RepositoryElem rn = (RepositoryElem) element;
+                               if (rn.isConnected()) {
+                                       String[] wkpNames = rn.getAccessibleWorkspaceNames();
+                                       if (element.getChildren().length != wkpNames.length)
+                                               doRefresh = true;
+                               }
+                       } else if (element instanceof RepositoriesElem) {
+                               doRefresh = true;
+                               // Always force refresh for RepositoriesElem : the condition
+                               // below does not take remote repository into account and it is
+                               // not trivial to do so.
+
+                               // RepositoriesElem rn = (RepositoriesElem) element;
+                               // if (element.getChildren().length !=
+                               // rn.getRepositoryRegister()
+                               // .getRepositories().size())
+                               // doRefresh = true;
+                       }
+                       if (doRefresh) {
+                               element.clearChildren();
+                               element.getChildren();
+                       }
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Unexpected error while synchronising the UI with the JCR repository", re);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrDClickListener.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrDClickListener.java
new file mode 100644 (file)
index 0000000..07a14d0
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.ui.jcr.model.RepositoryElem;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+
+/** Centralizes the management of double click on a NodeTreeViewer */
+public class JcrDClickListener implements IDoubleClickListener {
+       // private final static Log log = LogFactory
+       // .getLog(GenericNodeDoubleClickListener.class);
+
+       private TreeViewer nodeViewer;
+
+       // private JcrFileProvider jfp;
+       // private FileHandler fileHandler;
+
+       public JcrDClickListener(TreeViewer nodeViewer) {
+               this.nodeViewer = nodeViewer;
+               // jfp = new JcrFileProvider();
+               // Commented out. see https://www.argeo.org/bugzilla/show_bug.cgi?id=188
+               // fileHandler = null;
+               // fileHandler = new FileHandler(jfp);
+       }
+
+       public void doubleClick(DoubleClickEvent event) {
+               if (event.getSelection() == null || event.getSelection().isEmpty())
+                       return;
+               Object obj = ((IStructuredSelection) event.getSelection()).getFirstElement();
+               if (obj instanceof RepositoryElem) {
+                       RepositoryElem rpNode = (RepositoryElem) obj;
+                       if (rpNode.isConnected()) {
+                               rpNode.logout();
+                       } else {
+                               rpNode.login();
+                       }
+                       nodeViewer.refresh(obj);
+               } else if (obj instanceof WorkspaceElem) {
+                       WorkspaceElem wn = (WorkspaceElem) obj;
+                       if (wn.isConnected())
+                               wn.logout();
+                       else
+                               wn.login();
+                       nodeViewer.refresh(obj);
+               } else if (obj instanceof SingleJcrNodeElem) {
+                       SingleJcrNodeElem sjn = (SingleJcrNodeElem) obj;
+                       Node node = sjn.getNode();
+                       openNode(node);
+               }
+       }
+
+       protected void openNode(Node node) {
+               // TODO implement generic behaviour
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrImages.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrImages.java
new file mode 100644 (file)
index 0000000..d86edb5
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import org.argeo.cms.ui.theme.CmsImages;
+import org.eclipse.swt.graphics.Image;
+
+/** Shared icons. */
+public class JcrImages {
+       public final static Image NODE = CmsImages.createIcon("node.gif");
+       public final static Image FOLDER = CmsImages.createIcon("folder.gif");
+       public final static Image FILE = CmsImages.createIcon("file.gif");
+       public final static Image BINARY = CmsImages.createIcon("binary.png");
+       public final static Image HOME = CmsImages.createIcon("home.gif");
+       public final static Image SORT = CmsImages.createIcon("sort.gif");
+       public final static Image REMOVE = CmsImages.createIcon("remove.gif");
+
+       public final static Image REPOSITORIES = CmsImages.createIcon("repositories.gif");
+       public final static Image REPOSITORY_DISCONNECTED = CmsImages.createIcon("repository_disconnected.gif");
+       public final static Image REPOSITORY_CONNECTED = CmsImages.createIcon("repository_connected.gif");
+       public final static Image REMOTE_DISCONNECTED = CmsImages.createIcon("remote_disconnected.gif");
+       public final static Image REMOTE_CONNECTED = CmsImages.createIcon("remote_connected.gif");
+       public final static Image WORKSPACE_DISCONNECTED = CmsImages.createIcon("workspace_disconnected.png");
+       public final static Image WORKSPACE_CONNECTED = CmsImages.createIcon("workspace_connected.png");
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java
new file mode 100644 (file)
index 0000000..f45e876
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.jcr.utils.JcrItemsComparator;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Implementation of the {@code ITreeContentProvider} in order to display a
+ * single JCR node and its children in a tree like structure
+ */
+public class JcrTreeContentProvider implements ITreeContentProvider {
+       private static final long serialVersionUID = -2128326504754297297L;
+       // private Node rootNode;
+       private JcrItemsComparator itemComparator = new JcrItemsComparator();
+
+       /**
+        * Sends back the first level of the Tree. input element must be a single
+        * node object
+        */
+       public Object[] getElements(Object inputElement) {
+               try {
+                       Node rootNode = (Node) inputElement;
+                       List<Node> result = new ArrayList<Node>();
+                       NodeIterator ni = rootNode.getNodes();
+                       while (ni.hasNext())
+                               result.add(ni.nextNode());
+                       return result.toArray();
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Unable to retrieve elements for " + inputElement, re);
+               }
+       }
+
+       public Object[] getChildren(Object parentElement) {
+               return childrenNodes((Node) parentElement);
+       }
+
+       public Object getParent(Object element) {
+               try {
+                       Node node = (Node) element;
+                       if (!node.getPath().equals("/"))
+                               return node.getParent();
+                       else
+                               return null;
+               } catch (RepositoryException e) {
+                       return null;
+               }
+       }
+
+       public boolean hasChildren(Object element) {
+               try {
+                       return ((Node) element).hasNodes();
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot check children existence on " + element, e);
+               }
+       }
+
+       protected Object[] childrenNodes(Node parentNode) {
+               try {
+                       List<Node> children = new ArrayList<Node>();
+                       NodeIterator nit = parentNode.getNodes();
+                       while (nit.hasNext())
+                               children.add(nit.nextNode());
+                       Node[] arr = children.toArray(new Node[0]);
+                       Arrays.sort(arr, itemComparator);
+                       return arr;
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot list children of " + parentNode, e);
+               }
+       }
+
+       public void dispose() {
+       }
+
+       public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/NodeContentProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/NodeContentProvider.java
new file mode 100644 (file)
index 0000000..9cccb52
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.cms.ui.jcr.model.RepositoriesElem;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeUtils;
+import org.argeo.node.security.Keyring;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Implementation of the {@code ITreeContentProvider} to display multiple
+ * repository environment in a tree like structure
+ */
+public class NodeContentProvider implements ITreeContentProvider {
+       private static final long serialVersionUID = -4083809398848374403L;
+       final private RepositoryRegister repositoryRegister;
+       final private RepositoryFactory repositoryFactory;
+
+       // Current user session on the default workspace of the argeo Node
+       final private Session userSession;
+       final private Keyring keyring;
+       private boolean sortChildren;
+
+       // Reference for cleaning
+       private SingleJcrNodeElem homeNode = null;
+       private RepositoriesElem repositoriesNode = null;
+
+       // Utils
+       private TreeBrowserComparator itemComparator = new TreeBrowserComparator();
+
+       public NodeContentProvider(Session userSession, Keyring keyring,
+                       RepositoryRegister repositoryRegister,
+                       RepositoryFactory repositoryFactory, Boolean sortChildren) {
+               this.userSession = userSession;
+               this.keyring = keyring;
+               this.repositoryRegister = repositoryRegister;
+               this.repositoryFactory = repositoryFactory;
+               this.sortChildren = sortChildren;
+       }
+
+       public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               if (newInput == null)// dispose
+                       return;
+
+               if (userSession != null) {
+                       Node userHome = NodeUtils.getUserHome(userSession);
+                       if (userHome != null) {
+                               // TODO : find a way to dynamically get alias for the node
+                               if (homeNode != null)
+                                       homeNode.dispose();
+                               homeNode = new SingleJcrNodeElem(null, userHome,
+                                               userSession.getUserID(), NodeConstants.NODE);
+                       }
+               }
+               if (repositoryRegister != null) {
+                       if (repositoriesNode != null)
+                               repositoriesNode.dispose();
+                       repositoriesNode = new RepositoriesElem("Repositories",
+                                       repositoryRegister, repositoryFactory, null, userSession,
+                                       keyring);
+               }
+       }
+
+       /**
+        * Sends back the first level of the Tree. Independent from inputElement
+        * that can be null
+        */
+       public Object[] getElements(Object inputElement) {
+               List<Object> objs = new ArrayList<Object>();
+               if (homeNode != null)
+                       objs.add(homeNode);
+               if (repositoriesNode != null)
+                       objs.add(repositoriesNode);
+               return objs.toArray();
+       }
+
+       public Object[] getChildren(Object parentElement) {
+               if (parentElement instanceof TreeParent) {
+                       if (sortChildren) {
+                               Object[] tmpArr = ((TreeParent) parentElement).getChildren();
+                               if (tmpArr == null)
+                                       return new Object[0];
+                               TreeParent[] arr = new TreeParent[tmpArr.length];
+                               for (int i = 0; i < tmpArr.length; i++)
+                                       arr[i] = (TreeParent) tmpArr[i];
+                               Arrays.sort(arr, itemComparator);
+                               return arr;
+                       } else
+                               return ((TreeParent) parentElement).getChildren();
+               } else
+                       return new Object[0];
+       }
+
+       /**
+        * Sets whether the content provider should order the children nodes or not.
+        * It is user duty to call a full refresh of the tree after changing this
+        * parameter.
+        */
+       public void setSortChildren(boolean sortChildren) {
+               this.sortChildren = sortChildren;
+       }
+
+       public Object getParent(Object element) {
+               if (element instanceof TreeParent) {
+                       return ((TreeParent) element).getParent();
+               } else
+                       return null;
+       }
+
+       public boolean hasChildren(Object element) {
+               if (element instanceof RepositoriesElem) {
+                       RepositoryRegister rr = ((RepositoriesElem) element)
+                                       .getRepositoryRegister();
+                       return rr.getRepositories().size() > 0;
+               } else if (element instanceof TreeParent) {
+                       TreeParent tp = (TreeParent) element;
+                       return tp.hasChildren();
+               }
+               return false;
+       }
+
+       public void dispose() {
+               if (homeNode != null)
+                       homeNode.dispose();
+               if (repositoriesNode != null) {
+                       // logs out open sessions
+                       // see https://bugzilla.argeo.org/show_bug.cgi?id=23
+                       repositoriesNode.dispose();
+               }
+       }
+
+       /**
+        * Specific comparator for this view. See specification here:
+        * https://www.argeo.org/bugzilla/show_bug.cgi?id=139
+        */
+       private class TreeBrowserComparator implements Comparator<TreeParent> {
+
+               public int category(TreeParent element) {
+                       if (element instanceof SingleJcrNodeElem) {
+                               Node node = ((SingleJcrNodeElem) element).getNode();
+                               try {
+                                       if (node.isNodeType(NodeType.NT_FOLDER))
+                                               return 5;
+                               } catch (RepositoryException e) {
+                                       // TODO Auto-generated catch block
+                                       e.printStackTrace();
+                               }
+                       }
+                       return 10;
+               }
+
+               public int compare(TreeParent o1, TreeParent o2) {
+                       int cat1 = category(o1);
+                       int cat2 = category(o2);
+
+                       if (cat1 != cat2) {
+                               return cat1 - cat2;
+                       }
+                       return o1.getName().compareTo(o2.getName());
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java
new file mode 100644 (file)
index 0000000..765f320
--- /dev/null
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import javax.jcr.NamespaceException;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.ui.jcr.model.RemoteRepositoryElem;
+import org.argeo.cms.ui.jcr.model.RepositoriesElem;
+import org.argeo.cms.ui.jcr.model.RepositoryElem;
+import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem;
+import org.argeo.cms.ui.jcr.model.WorkspaceElem;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeTypes;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/** Provides reasonable defaults for know JCR types. */
+public class NodeLabelProvider extends ColumnLabelProvider {
+       private static final long serialVersionUID = -3662051696443321843L;
+
+       private final static Log log = LogFactory.getLog(NodeLabelProvider.class);
+
+       public String getText(Object element) {
+               try {
+                       if (element instanceof SingleJcrNodeElem) {
+                               SingleJcrNodeElem sjn = (SingleJcrNodeElem) element;
+                               return getText(sjn.getNode());
+                       } else if (element instanceof Node) {
+                               return getText((Node) element);
+                       } else
+                               return super.getText(element);
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Unexpected JCR error while getting node name.");
+               }
+       }
+
+       protected String getText(Node node) throws RepositoryException {
+               String label = node.getName();
+               StringBuffer mixins = new StringBuffer("");
+               for (NodeType type : node.getMixinNodeTypes())
+                       mixins.append(' ').append(type.getName());
+
+               return label + " [" + node.getPrimaryNodeType().getName() + mixins + "]";
+       }
+
+       @Override
+       public Image getImage(Object element) {
+               if (element instanceof RemoteRepositoryElem) {
+                       if (((RemoteRepositoryElem) element).isConnected())
+                               return JcrImages.REMOTE_CONNECTED;
+                       else
+                               return JcrImages.REMOTE_DISCONNECTED;
+               } else if (element instanceof RepositoryElem) {
+                       if (((RepositoryElem) element).isConnected())
+                               return JcrImages.REPOSITORY_CONNECTED;
+                       else
+                               return JcrImages.REPOSITORY_DISCONNECTED;
+               } else if (element instanceof WorkspaceElem) {
+                       if (((WorkspaceElem) element).isConnected())
+                               return JcrImages.WORKSPACE_CONNECTED;
+                       else
+                               return JcrImages.WORKSPACE_DISCONNECTED;
+               } else if (element instanceof RepositoriesElem) {
+                       return JcrImages.REPOSITORIES;
+               } else if (element instanceof SingleJcrNodeElem) {
+                       Node nodeElem = ((SingleJcrNodeElem) element).getNode();
+                       return getImage(nodeElem);
+
+                       // if (element instanceof Node) {
+                       // return getImage((Node) element);
+                       // } else if (element instanceof WrappedNode) {
+                       // return getImage(((WrappedNode) element).getNode());
+                       // } else if (element instanceof NodesWrapper) {
+                       // return getImage(((NodesWrapper) element).getNode());
+                       // }
+               }
+               // try {
+               // return super.getImage();
+               // } catch (RepositoryException e) {
+               // return null;
+               // }
+               return super.getImage(element);
+       }
+
+       protected Image getImage(Node node) {
+               try {
+                       if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FILE))
+                               return JcrImages.FILE;
+                       else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER))
+                               return JcrImages.FOLDER;
+                       else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_RESOURCE))
+                               return JcrImages.BINARY;
+                       try {
+                               // optimizes
+                               if (node.hasProperty(LdapAttrs.uid.property()) && node.isNodeType(NodeTypes.NODE_USER_HOME))
+                                       return JcrImages.HOME;
+                       } catch (NamespaceException e) {
+                               // node namespace is not registered in this repo
+                       }
+                       return JcrImages.NODE;
+               } catch (RepositoryException e) {
+                       log.warn("Error while retrieving type for " + node + " in order to display corresponding image");
+                       e.printStackTrace();
+                       return null;
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java
new file mode 100644 (file)
index 0000000..444350a
--- /dev/null
@@ -0,0 +1,52 @@
+package org.argeo.cms.ui.jcr;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Repository;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class OsgiRepositoryRegister extends DefaultRepositoryRegister {
+       private final static BundleContext bc = FrameworkUtil.getBundle(OsgiRepositoryRegister.class).getBundleContext();
+       private final ServiceTracker<Repository, Repository> repositoryTracker;
+
+       public OsgiRepositoryRegister() {
+               repositoryTracker = new ServiceTracker<Repository, Repository>(bc, Repository.class, null) {
+
+                       @Override
+                       public Repository addingService(ServiceReference<Repository> reference) {
+
+                               Repository repository = super.addingService(reference);
+                               Map<String, Object> props = new HashMap<>();
+                               for (String key : reference.getPropertyKeys()) {
+                                       props.put(key, reference.getProperty(key));
+                               }
+                               register(repository, props);
+                               return repository;
+                       }
+
+                       @Override
+                       public void removedService(ServiceReference<Repository> reference, Repository service) {
+                               Map<String, Object> props = new HashMap<>();
+                               for (String key : reference.getPropertyKeys()) {
+                                       props.put(key, reference.getProperty(key));
+                               }
+                               unregister(service, props);
+                               super.removedService(reference, service);
+                       }
+
+               };
+       }
+
+       public void init() {
+               repositoryTracker.open();
+       }
+
+       public void destroy() {
+               repositoryTracker.close();
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java
new file mode 100644 (file)
index 0000000..d67b133
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.jcr.utils.JcrItemsComparator;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/** Simple content provider that displays all properties of a given Node */
+public class PropertiesContentProvider implements IStructuredContentProvider {
+       private static final long serialVersionUID = 5227554668841613078L;
+       private JcrItemsComparator itemComparator = new JcrItemsComparator();
+
+       public void dispose() {
+       }
+
+       public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+       }
+
+       public Object[] getElements(Object inputElement) {
+               try {
+                       if (inputElement instanceof Node) {
+                               Set<Property> props = new TreeSet<Property>(itemComparator);
+                               PropertyIterator pit = ((Node) inputElement).getProperties();
+                               while (pit.hasNext())
+                                       props.add(pit.nextProperty());
+                               return props.toArray();
+                       }
+                       return new Object[] {};
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot get element for "
+                                       + inputElement, e);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java
new file mode 100644 (file)
index 0000000..3274fd1
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+import org.argeo.cms.ui.CmsConstants;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ViewerCell;
+
+/** Default basic label provider for a given JCR Node's properties */
+public class PropertyLabelProvider extends ColumnLabelProvider {
+       private static final long serialVersionUID = -5405794508731390147L;
+
+       // To be able to change column order easily
+       public static final int COLUMN_PROPERTY = 0;
+       public static final int COLUMN_VALUE = 1;
+       public static final int COLUMN_TYPE = 2;
+       public static final int COLUMN_ATTRIBUTES = 3;
+
+       // Utils
+       protected DateFormat timeFormatter = new SimpleDateFormat(CmsConstants.DATE_TIME_FORMAT);
+
+       public void update(ViewerCell cell) {
+               Object element = cell.getElement();
+               cell.setText(getColumnText(element, cell.getColumnIndex()));
+       }
+
+       public String getColumnText(Object element, int columnIndex) {
+               try {
+                       if (element instanceof Property) {
+                               Property prop = (Property) element;
+                               if (prop.isMultiple()) {
+                                       switch (columnIndex) {
+                                       case COLUMN_PROPERTY:
+                                               return prop.getName();
+                                       case COLUMN_VALUE:
+                                               // Corresponding values are listed on children
+                                               return "";
+                                       case COLUMN_TYPE:
+                                               return JcrBrowserUtils.getPropertyTypeAsString(prop);
+                                       case COLUMN_ATTRIBUTES:
+                                               return JcrUtils.getPropertyDefinitionAsString(prop);
+                                       }
+                               } else {
+                                       switch (columnIndex) {
+                                       case COLUMN_PROPERTY:
+                                               return prop.getName();
+                                       case COLUMN_VALUE:
+                                               return formatValueAsString(prop.getValue());
+                                       case COLUMN_TYPE:
+                                               return JcrBrowserUtils.getPropertyTypeAsString(prop);
+                                       case COLUMN_ATTRIBUTES:
+                                               return JcrUtils.getPropertyDefinitionAsString(prop);
+                                       }
+                               }
+                       } else if (element instanceof Value) {
+                               Value val = (Value) element;
+                               switch (columnIndex) {
+                               case COLUMN_PROPERTY:
+                                       // Nothing to show
+                                       return "";
+                               case COLUMN_VALUE:
+                                       return formatValueAsString(val);
+                               case COLUMN_TYPE:
+                                       // listed on the parent
+                                       return "";
+                               case COLUMN_ATTRIBUTES:
+                                       // Corresponding attributes are listed on the parent
+                                       return "";
+                               }
+                       }
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot retrieve prop value on " + element, re);
+               }
+               return null;
+       }
+
+       private String formatValueAsString(Value value) {
+               // TODO enhance this method
+               try {
+                       String strValue;
+
+                       if (value.getType() == PropertyType.BINARY)
+                               strValue = "<binary>";
+                       else if (value.getType() == PropertyType.DATE)
+                               strValue = timeFormatter.format(value.getDate().getTime());
+                       else
+                               strValue = value.getString();
+                       return strValue;
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("unexpected error while formatting value", e);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/RepositoryRegister.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/RepositoryRegister.java
new file mode 100644 (file)
index 0000000..b6b14d5
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryFactory;
+
+/** Allows to register repositories by name. */
+public interface RepositoryRegister extends RepositoryFactory {
+       /**
+        * The registered {@link Repository} as a read-only map. Note that this
+        * method should be called for each access in order to be sure to be up to
+        * date in case repositories have registered/unregistered
+        */
+       public Map<String, Repository> getRepositories();
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java
new file mode 100644 (file)
index 0000000..4d7746c
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.version.Version;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+
+/**
+ * Simple wrapping of the ColumnLabelProvider class to provide text display in
+ * order to build a tree for version. The getText() method does not assume that
+ * {@link Version} extends {@link Node} class to respect JCR 2.0 specification
+ * 
+ */
+public class VersionLabelProvider extends ColumnLabelProvider {
+       private static final long serialVersionUID = 5270739851193688238L;
+
+       public String getText(Object element) {
+               try {
+                       if (element instanceof Version) {
+                               Version version = (Version) element;
+                               return version.getName();
+                       } else if (element instanceof Node) {
+                               return ((Node) element).getName();
+                       }
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException(
+                                       "Unexpected error while getting element name", re);
+               }
+               return super.getText(element);
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java
new file mode 100644 (file)
index 0000000..61654b6
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.cms.ui.jcr.model;
+
+import javax.jcr.Repository;
+
+import org.argeo.eclipse.ui.TreeParent;
+
+/** Wrap a MaintainedRepository */
+public class MaintainedRepositoryElem extends RepositoryElem {
+
+       public MaintainedRepositoryElem(String alias, Repository repository, TreeParent parent) {
+               super(alias, repository, parent);
+               // if (!(repository instanceof MaintainedRepository)) {
+               // throw new ArgeoException("Repository " + alias
+               // + " is not a maintained repository");
+               // }
+       }
+
+       // protected MaintainedRepository getMaintainedRepository() {
+       // return (MaintainedRepository) getRepository();
+       // }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java
new file mode 100644 (file)
index 0000000..ad173cf
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr.model;
+
+import java.util.Arrays;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.node.NodeUtils;
+import org.argeo.node.security.Keyring;
+
+/** Root of a remote repository */
+public class RemoteRepositoryElem extends RepositoryElem {
+       private final Keyring keyring;
+       /**
+        * A session of the logged in user on the default workspace of the node
+        * repository.
+        */
+       private final Session userSession;
+       private final String remoteNodePath;
+
+       private final RepositoryFactory repositoryFactory;
+       private final String uri;
+
+       public RemoteRepositoryElem(String alias, RepositoryFactory repositoryFactory, String uri, TreeParent parent,
+                       Session userSession, Keyring keyring, String remoteNodePath) {
+               super(alias, null, parent);
+               this.repositoryFactory = repositoryFactory;
+               this.uri = uri;
+               this.keyring = keyring;
+               this.userSession = userSession;
+               this.remoteNodePath = remoteNodePath;
+       }
+
+       @Override
+       protected Session repositoryLogin(String workspaceName) throws RepositoryException {
+               Node remoteRepository = userSession.getNode(remoteNodePath);
+               String userID = remoteRepository.getProperty(ArgeoNames.ARGEO_USER_ID).getString();
+               if (userID.trim().equals("")) {
+                       return getRepository().login(workspaceName);
+               } else {
+                       String pwdPath = remoteRepository.getPath() + '/' + ArgeoNames.ARGEO_PASSWORD;
+                       char[] password = keyring.getAsChars(pwdPath);
+                       try {
+                               SimpleCredentials credentials = new SimpleCredentials(userID, password);
+                               return getRepository().login(credentials, workspaceName);
+                       } finally {
+                               Arrays.fill(password, 0, password.length, ' ');
+                       }
+               }
+       }
+
+       @Override
+       public Repository getRepository() {
+               if (repository == null)
+                       repository = NodeUtils.getRepositoryByUri(repositoryFactory, uri);
+               return super.getRepository();
+       }
+
+       public void remove() {
+               try {
+                       Node remoteNode = userSession.getNode(remoteNodePath);
+                       remoteNode.remove();
+                       remoteNode.getSession().save();
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot remove " + remoteNodePath, e);
+               }
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java
new file mode 100644 (file)
index 0000000..c772424
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr.model;
+
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.ui.jcr.RepositoryRegister;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.eclipse.ui.dialogs.ErrorFeedback;
+import org.argeo.node.NodeUtils;
+import org.argeo.node.security.Keyring;
+
+/**
+ * UI Tree component that implements the Argeo abstraction of a
+ * {@link RepositoryFactory} that enable a user to "mount" various repositories
+ * in a single Tree like View. It is usually meant to be at the root of the UI
+ * Tree and thus {@link getParent()} method will return null.
+ * 
+ * The {@link RepositoryFactory} is injected at instantiation time and must be
+ * use get or register new {@link Repository} objects upon which a reference is
+ * kept here.
+ */
+
+public class RepositoriesElem extends TreeParent implements ArgeoNames {
+       private final RepositoryRegister repositoryRegister;
+       private final RepositoryFactory repositoryFactory;
+
+       /**
+        * A session of the logged in user on the default workspace of the node
+        * repository.
+        */
+       private final Session userSession;
+       private final Keyring keyring;
+
+       public RepositoriesElem(String name, RepositoryRegister repositoryRegister, RepositoryFactory repositoryFactory,
+                       TreeParent parent, Session userSession, Keyring keyring) {
+               super(name);
+               this.repositoryRegister = repositoryRegister;
+               this.repositoryFactory = repositoryFactory;
+               this.userSession = userSession;
+               this.keyring = keyring;
+       }
+
+       /**
+        * Override normal behavior to initialize the various repositories only at
+        * request time
+        */
+       @Override
+       public synchronized Object[] getChildren() {
+               if (isLoaded()) {
+                       return super.getChildren();
+               } else {
+                       // initialize current object
+                       Map<String, Repository> refRepos = repositoryRegister.getRepositories();
+                       for (String name : refRepos.keySet()) {
+                               Repository repository = refRepos.get(name);
+                               // if (repository instanceof MaintainedRepository)
+                               // super.addChild(new MaintainedRepositoryElem(name,
+                               // repository, this));
+                               // else
+                               super.addChild(new RepositoryElem(name, repository, this));
+                       }
+
+                       // remote
+                       if (keyring != null) {
+                               try {
+                                       addRemoteRepositories(keyring);
+                               } catch (RepositoryException e) {
+                                       throw new EclipseUiException("Cannot browse remote repositories", e);
+                               }
+                       }
+                       return super.getChildren();
+               }
+       }
+
+       protected void addRemoteRepositories(Keyring jcrKeyring) throws RepositoryException {
+               Node userHome = NodeUtils.getUserHome(userSession);
+               if (userHome != null && userHome.hasNode(ARGEO_REMOTE)) {
+                       NodeIterator it = userHome.getNode(ARGEO_REMOTE).getNodes();
+                       while (it.hasNext()) {
+                               Node remoteNode = it.nextNode();
+                               String uri = remoteNode.getProperty(ARGEO_URI).getString();
+                               try {
+                                       RemoteRepositoryElem remoteRepositoryNode = new RemoteRepositoryElem(remoteNode.getName(),
+                                                       repositoryFactory, uri, this, userSession, jcrKeyring, remoteNode.getPath());
+                                       super.addChild(remoteRepositoryNode);
+                               } catch (Exception e) {
+                                       ErrorFeedback.show("Cannot add remote repository " + remoteNode, e);
+                               }
+                       }
+               }
+       }
+
+       public void registerNewRepository(String alias, Repository repository) {
+               // TODO: implement this
+               // Create a new RepositoryNode Object
+               // add it
+               // super.addChild(new RepositoriesNode(...));
+       }
+
+       /** Returns the {@link RepositoryRegister} wrapped by this object. */
+       public RepositoryRegister getRepositoryRegister() {
+               return repositoryRegister;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java
new file mode 100644 (file)
index 0000000..6f2288c
--- /dev/null
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr.model;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.jcr.JcrUtils;
+
+/**
+ * UI Tree component that wraps a JCR {@link Repository}. It also keeps a
+ * reference to its parent Tree Ui component; typically the unique
+ * {@link RepositoriesElem} object of the current view to enable bi-directionnal
+ * browsing in the tree.
+ */
+
+public class RepositoryElem extends TreeParent {
+       private String alias;
+       protected Repository repository;
+       private Session defaultSession = null;
+
+       /** Create a new repository with distinct name and alias */
+       public RepositoryElem(String alias, Repository repository, TreeParent parent) {
+               super(alias);
+               this.repository = repository;
+               setParent(parent);
+               this.alias = alias;
+       }
+
+       public void login() {
+               try {
+                       defaultSession = repositoryLogin("main");
+                       String[] wkpNames = defaultSession.getWorkspace().getAccessibleWorkspaceNames();
+                       for (String wkpName : wkpNames) {
+                               if (wkpName.equals(defaultSession.getWorkspace().getName()))
+                                       addChild(new WorkspaceElem(this, wkpName, defaultSession));
+                               else
+                                       addChild(new WorkspaceElem(this, wkpName));
+                       }
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot connect to repository " + alias, e);
+               }
+       }
+
+       public synchronized void logout() {
+               for (Object child : getChildren()) {
+                       if (child instanceof WorkspaceElem)
+                               ((WorkspaceElem) child).logout();
+               }
+               clearChildren();
+               JcrUtils.logoutQuietly(defaultSession);
+               defaultSession = null;
+       }
+
+       /**
+        * Actual call to the {@link Repository#login(javax.jcr.Credentials, String)}
+        * method. To be overridden.
+        */
+       protected Session repositoryLogin(String workspaceName) throws RepositoryException {
+               return repository.login(workspaceName);
+       }
+
+       public String[] getAccessibleWorkspaceNames() {
+               try {
+                       return defaultSession.getWorkspace().getAccessibleWorkspaceNames();
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot retrieve workspace names", e);
+               }
+       }
+
+       public void createWorkspace(String workspaceName) {
+               if (!isConnected())
+                       login();
+               try {
+                       defaultSession.getWorkspace().createWorkspace(workspaceName);
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot create workspace", e);
+               }
+       }
+
+       /** returns the {@link Repository} referenced by the current UI Node */
+       public Repository getRepository() {
+               return repository;
+       }
+
+       public String getAlias() {
+               return alias;
+       }
+
+       public Boolean isConnected() {
+               if (defaultSession != null && defaultSession.isLive())
+                       return true;
+               else
+                       return false;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java
new file mode 100644 (file)
index 0000000..14c78c0
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr.model;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Workspace;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+
+/**
+ * UI Tree component. Wraps a node of a JCR {@link Workspace}. It also keeps a
+ * reference to its parent node that can either be a {@link WorkspaceElem}, a
+ * {@link SingleJcrNodeElem} or null if the node is "mounted" as the root of the
+ * UI tree.
+ */
+public class SingleJcrNodeElem extends TreeParent {
+
+       private final Node node;
+       private String alias = null;
+
+       /** Creates a new UiNode in the UI Tree */
+       public SingleJcrNodeElem(TreeParent parent, Node node, String name) {
+               super(name);
+               setParent(parent);
+               this.node = node;
+       }
+
+       /**
+        * Creates a new UiNode in the UI Tree, keeping a reference to the alias of
+        * the corresponding repository in the current UI environment. It is useful
+        * to be able to mount nodes as roots of the UI tree.
+        */
+       public SingleJcrNodeElem(TreeParent parent, Node node, String name, String alias) {
+               super(name);
+               setParent(parent);
+               this.node = node;
+               this.alias = alias;
+       }
+
+       /** Returns the node wrapped by the current UI object */
+       public Node getNode() {
+               return node;
+       }
+
+       protected String getRepositoryAlias() {
+               return alias;
+       }
+
+       /**
+        * Overrides normal behaviour to initialise children only when first
+        * requested
+        */
+       @Override
+       public synchronized Object[] getChildren() {
+               if (isLoaded()) {
+                       return super.getChildren();
+               } else {
+                       // initialize current object
+                       try {
+                               NodeIterator ni = node.getNodes();
+                               while (ni.hasNext()) {
+                                       Node curNode = ni.nextNode();
+                                       addChild(new SingleJcrNodeElem(this, curNode, curNode.getName()));
+                               }
+                               return super.getChildren();
+                       } catch (RepositoryException re) {
+                               throw new EclipseUiException("Cannot initialize SingleJcrNode children", re);
+                       }
+               }
+       }
+
+       @Override
+       public boolean hasChildren() {
+               try {
+                       if (node.getSession().isLive())
+                               return node.hasNodes();
+                       else
+                               return false;
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot check children node existence", re);
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java
new file mode 100644 (file)
index 0000000..45cda80
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.jcr.model;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+// import javax.jcr.Workspace;
+import javax.jcr.Workspace;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.TreeParent;
+import org.argeo.jcr.JcrUtils;
+
+/**
+ * UI Tree component. Wraps the root node of a JCR {@link Workspace}. It also
+ * keeps a reference to its parent {@link RepositoryElem}, to be able to
+ * retrieve alias of the current used repository
+ */
+public class WorkspaceElem extends TreeParent {
+       private Session session = null;
+
+       public WorkspaceElem(RepositoryElem parent, String name) {
+               this(parent, name, null);
+       }
+
+       public WorkspaceElem(RepositoryElem parent, String name, Session session) {
+               super(name);
+               this.session = session;
+               setParent(parent);
+       }
+
+       public synchronized Session getSession() {
+               return session;
+       }
+
+       public synchronized Node getRootNode() {
+               try {
+                       if (session != null)
+                               return session.getRootNode();
+                       else
+                               return null;
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot get root node of workspace "
+                                       + getName(), e);
+               }
+       }
+
+       public synchronized void login() {
+               try {
+                       session = ((RepositoryElem) getParent()).repositoryLogin(getName());
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot connect to repository "
+                                       + getName(), e);
+               }
+       }
+
+       public Boolean isConnected() {
+               if (session != null && session.isLive())
+                       return true;
+               else
+                       return false;
+       }
+
+       @Override
+       public synchronized void dispose() {
+               logout();
+               super.dispose();
+       }
+
+       /** Logouts the session, does not nothing if there is no live session. */
+       public synchronized void logout() {
+               clearChildren();
+               JcrUtils.logoutQuietly(session);
+               session = null;
+       }
+
+       @Override
+       public synchronized boolean hasChildren() {
+               try {
+                       if (isConnected())
+                               return session.getRootNode().hasNodes();
+                       else
+                               return false;
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException(
+                                       "Unexpected error while checking children node existence",
+                                       re);
+               }
+       }
+
+       /** Override normal behaviour to initialize display of the workspace */
+       @Override
+       public synchronized Object[] getChildren() {
+               if (isLoaded()) {
+                       return super.getChildren();
+               } else {
+                       // initialize current object
+                       try {
+                               Node rootNode;
+                               if (session == null)
+                                       return null;
+                               else
+                                       rootNode = session.getRootNode();
+                               NodeIterator ni = rootNode.getNodes();
+                               while (ni.hasNext()) {
+                                       Node node = ni.nextNode();
+                                       addChild(new SingleJcrNodeElem(this, node, node.getName()));
+                               }
+                               return super.getChildren();
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException(
+                                               "Cannot initialize WorkspaceNode UI object."
+                                                               + getName(), e);
+                       }
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/useradmin/PickUpUserDialog.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/useradmin/PickUpUserDialog.java
new file mode 100644 (file)
index 0000000..36a1da2
--- /dev/null
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.useradmin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.parts.LdifUsersTable;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.LdapObjs;
+import org.argeo.node.NodeConstants;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TrayDialog;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+/** Dialog with a user (or group) list to pick up one */
+public class PickUpUserDialog extends TrayDialog {
+       private static final long serialVersionUID = -1420106871173920369L;
+
+       // Business objects
+       private final UserAdmin userAdmin;
+       private User selectedUser;
+
+       // this page widgets and UI objects
+       private String title;
+       private LdifUsersTable userTableViewerCmp;
+       private TableViewer userViewer;
+       private List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+
+       /**
+        * A dialog to pick up a group or a user, showing a table with default
+        * columns
+        */
+       public PickUpUserDialog(Shell parentShell, String title, UserAdmin userAdmin) {
+               super(parentShell);
+               this.title = title;
+               this.userAdmin = userAdmin;
+
+               columnDefs.add(new ColumnDefinition(new UserLP(UserLP.COL_ICON), "",
+                               24, 24));
+               columnDefs.add(new ColumnDefinition(
+                               new UserLP(UserLP.COL_DISPLAY_NAME), "Common Name", 150, 100));
+               columnDefs.add(new ColumnDefinition(new UserLP(UserLP.COL_DOMAIN),
+                               "Domain", 100, 120));
+               columnDefs.add(new ColumnDefinition(new UserLP(UserLP.COL_DN),
+                               "Distinguished Name", 300, 100));
+       }
+
+       /** A dialog to pick up a group or a user */
+       public PickUpUserDialog(Shell parentShell, String title,
+                       UserAdmin userAdmin, List<ColumnDefinition> columnDefs) {
+               super(parentShell);
+               this.title = title;
+               this.userAdmin = userAdmin;
+               this.columnDefs = columnDefs;
+       }
+
+       @Override
+       protected void okPressed() {
+               if (getSelected() == null)
+                       MessageDialog.openError(getShell(), "No user chosen",
+                                       "Please, choose a user or press Cancel.");
+               else
+                       super.okPressed();
+       }
+
+       protected Control createDialogArea(Composite parent) {
+               Composite dialogArea = (Composite) super.createDialogArea(parent);
+               dialogArea.setLayout(new FillLayout());
+
+               Composite bodyCmp = new Composite(dialogArea, SWT.NO_FOCUS);
+               bodyCmp.setLayout(new GridLayout());
+
+               // Create and configure the table
+               userTableViewerCmp = new MyUserTableViewer(bodyCmp, SWT.MULTI
+                               | SWT.H_SCROLL | SWT.V_SCROLL);
+
+               userTableViewerCmp.setColumnDefinitions(columnDefs);
+               userTableViewerCmp.populateWithStaticFilters(false, false);
+               GridData gd = EclipseUiUtils.fillAll();
+               gd.minimumHeight = 300;
+               userTableViewerCmp.setLayoutData(gd);
+               userTableViewerCmp.refresh();
+
+               // Controllers
+               userViewer = userTableViewerCmp.getTableViewer();
+               userViewer.addDoubleClickListener(new MyDoubleClickListener());
+               userViewer
+                               .addSelectionChangedListener(new MySelectionChangedListener());
+
+               parent.pack();
+               return dialogArea;
+       }
+
+       public User getSelected() {
+               if (selectedUser == null)
+                       return null;
+               else
+                       return selectedUser;
+       }
+
+       protected void configureShell(Shell shell) {
+               super.configureShell(shell);
+               shell.setText(title);
+       }
+
+       class MyDoubleClickListener implements IDoubleClickListener {
+               public void doubleClick(DoubleClickEvent evt) {
+                       if (evt.getSelection().isEmpty())
+                               return;
+
+                       Object obj = ((IStructuredSelection) evt.getSelection())
+                                       .getFirstElement();
+                       if (obj instanceof User) {
+                               selectedUser = (User) obj;
+                               okPressed();
+                       }
+               }
+       }
+
+       class MySelectionChangedListener implements ISelectionChangedListener {
+               @Override
+               public void selectionChanged(SelectionChangedEvent event) {
+                       if (event.getSelection().isEmpty()) {
+                               selectedUser = null;
+                               return;
+                       }
+                       Object obj = ((IStructuredSelection) event.getSelection())
+                                       .getFirstElement();
+                       if (obj instanceof Group) {
+                               selectedUser = (Group) obj;
+                       }
+               }
+       }
+
+       private class MyUserTableViewer extends LdifUsersTable {
+               private static final long serialVersionUID = 8467999509931900367L;
+
+               private final String[] knownProps = { LdapAttrs.uid.name(),
+                               LdapAttrs.cn.name(), LdapAttrs.DN };
+
+               private Button showSystemRoleBtn;
+               private Button showUserBtn;
+
+               public MyUserTableViewer(Composite parent, int style) {
+                       super(parent, style);
+               }
+
+               protected void populateStaticFilters(Composite staticFilterCmp) {
+                       staticFilterCmp.setLayout(new GridLayout());
+                       showSystemRoleBtn = new Button(staticFilterCmp, SWT.CHECK);
+                       showSystemRoleBtn.setText("Show system roles  ");
+
+                       showUserBtn = new Button(staticFilterCmp, SWT.CHECK);
+                       showUserBtn.setText("Show users  ");
+
+                       SelectionListener sl = new SelectionAdapter() {
+                               private static final long serialVersionUID = -7033424592697691676L;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       refresh();
+                               }
+                       };
+
+                       showSystemRoleBtn.addSelectionListener(sl);
+                       showUserBtn.addSelectionListener(sl);
+               }
+
+               @Override
+               protected List<User> listFilteredElements(String filter) {
+                       Role[] roles;
+                       try {
+                               StringBuilder builder = new StringBuilder();
+
+                               StringBuilder filterBuilder = new StringBuilder();
+                               if (notNull(filter))
+                                       for (String prop : knownProps) {
+                                               filterBuilder.append("(");
+                                               filterBuilder.append(prop);
+                                               filterBuilder.append("=*");
+                                               filterBuilder.append(filter);
+                                               filterBuilder.append("*)");
+                                       }
+
+                               String typeStr = "(" + LdapAttrs.objectClass.name() + "="
+                                               + LdapObjs.groupOfNames.name() + ")";
+                               if ((showUserBtn.getSelection()))
+                                       typeStr = "(|(" + LdapAttrs.objectClass.name() + "="
+                                                       + LdapObjs.inetOrgPerson.name() + ")" + typeStr
+                                                       + ")";
+
+                               if (!showSystemRoleBtn.getSelection())
+                                       typeStr = "(& " + typeStr + "(!(" + LdapAttrs.DN + "=*"
+                                                       + NodeConstants.ROLES_BASEDN + ")))";
+
+                               if (filterBuilder.length() > 1) {
+                                       builder.append("(&" + typeStr);
+                                       builder.append("(|");
+                                       builder.append(filterBuilder.toString());
+                                       builder.append("))");
+                               } else {
+                                       builder.append(typeStr);
+                               }
+                               roles = userAdmin.getRoles(builder.toString());
+                       } catch (InvalidSyntaxException e) {
+                               throw new EclipseUiException(
+                                               "Unable to get roles with filter: " + filter, e);
+                       }
+                       List<User> users = new ArrayList<User>();
+                       for (Role role : roles)
+                               if (!users.contains(role))
+                                       users.add((User) role);
+                       return users;
+               }
+       }
+
+       private boolean notNull(String string) {
+               if (string == null)
+                       return false;
+               else
+                       return !"".equals(string.trim());
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/useradmin/UserLP.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/useradmin/UserLP.java
new file mode 100644 (file)
index 0000000..2344590
--- /dev/null
@@ -0,0 +1,76 @@
+package org.argeo.cms.ui.useradmin;
+
+import org.argeo.cms.util.UserAdminUtils;
+import org.argeo.node.NodeConstants;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/** Centralize label providers for the group table */
+class UserLP extends ColumnLabelProvider {
+       private static final long serialVersionUID = -4645930210988368571L;
+
+       final static String COL_ICON = "colID.icon";
+       final static String COL_DN = "colID.dn";
+       final static String COL_DISPLAY_NAME = "colID.displayName";
+       final static String COL_DOMAIN = "colID.domain";
+
+       final String currType;
+
+       // private Font italic;
+       private Font bold;
+
+       UserLP(String colId) {
+               this.currType = colId;
+       }
+
+       @Override
+       public Font getFont(Object element) {
+               // Current user as bold
+               if (UserAdminUtils.isCurrentUser(((User) element))) {
+                       if (bold == null)
+                               bold = JFaceResources.getFontRegistry().defaultFontDescriptor().setStyle(SWT.BOLD)
+                                               .createFont(Display.getCurrent());
+                       return bold;
+               }
+               return null;
+       }
+
+       @Override
+       public Image getImage(Object element) {
+               if (COL_ICON.equals(currType)) {
+                       User user = (User) element;
+                       String dn = user.getName();
+                       if (dn.endsWith(NodeConstants.ROLES_BASEDN))
+                               return UsersImages.ICON_ROLE;
+                       else if (user.getType() == Role.GROUP)
+                               return UsersImages.ICON_GROUP;
+                       else
+                               return UsersImages.ICON_USER;
+               } else
+                       return null;
+       }
+
+       @Override
+       public String getText(Object element) {
+               User user = (User) element;
+               return getText(user);
+
+       }
+
+       public String getText(User user) {
+               if (COL_DN.equals(currType))
+                       return user.getName();
+               else if (COL_DISPLAY_NAME.equals(currType))
+                       return UserAdminUtils.getCommonName(user);
+               else if (COL_DOMAIN.equals(currType))
+                       return UserAdminUtils.getDomainName(user);
+               else
+                       return "";
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/useradmin/UsersImages.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/useradmin/UsersImages.java
new file mode 100644 (file)
index 0000000..17ede56
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.ui.useradmin;
+
+import org.argeo.cms.ui.theme.CmsImages;
+import org.eclipse.swt.graphics.Image;
+
+/** Specific users icons. */
+public class UsersImages {
+       private final static String PREFIX = "icons/";
+
+       public final static Image ICON_USER = CmsImages.createImg(PREFIX + "person.png");
+       public final static Image ICON_GROUP = CmsImages.createImg(PREFIX + "group.png");
+       public final static Image ICON_ROLE = CmsImages.createImg(PREFIX + "role.gif");
+       public final static Image ICON_CHANGE_PASSWORD = CmsImages.createImg(PREFIX + "security.gif");
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/BundleResourceLoader.java b/org.argeo.cms.ui/src/org/argeo/cms/util/BundleResourceLoader.java
new file mode 100644 (file)
index 0000000..7342e10
--- /dev/null
@@ -0,0 +1,34 @@
+package org.argeo.cms.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import org.argeo.cms.CmsException;
+import org.eclipse.rap.rwt.service.ResourceLoader;
+import org.osgi.framework.Bundle;
+
+/** {@link ResourceLoader} implementation wrapping an {@link Bundle}. */
+public class BundleResourceLoader implements ResourceLoader {
+       private final Bundle bundle;
+
+       public BundleResourceLoader(Bundle bundle) {
+               this.bundle = bundle;
+       }
+
+       @Override
+       public InputStream getResourceAsStream(String resourceName) throws IOException {
+               URL res = bundle.getEntry(resourceName);
+               if (res == null) {
+                       res = bundle.getResource(resourceName);
+                       if (res == null)
+                               throw new CmsException("Resource " + resourceName + " not found in bundle " + bundle.getSymbolicName());
+               }
+               return res.openStream();
+       }
+
+       public Bundle getBundle() {
+               return bundle;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/CmsLink.java b/org.argeo.cms.ui/src/org/argeo/cms/util/CmsLink.java
new file mode 100644 (file)
index 0000000..faf0118
--- /dev/null
@@ -0,0 +1,274 @@
+package org.argeo.cms.util;
+
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.CmsStyles;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.node.NodeUtils;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.service.ResourceManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.osgi.framework.BundleContext;
+
+/** A link to an internal or external location. */
+public class CmsLink implements CmsUiProvider {
+       private final static Log log = LogFactory.getLog(CmsLink.class);
+       private BundleContext bundleContext;
+
+       private String label;
+       private String custom;
+       private String target;
+       private String image;
+       private MouseListener mouseListener;
+
+       private int horizontalAlignment = SWT.CENTER;
+       private int verticalAlignment = SWT.CENTER;
+
+       private String loggedInLabel = null;
+       private String loggedInTarget = null;
+
+       // internal
+       // private Boolean isUrl = false;
+       private Integer imageWidth, imageHeight;
+
+       public CmsLink() {
+               super();
+       }
+
+       public CmsLink(String label, String target) {
+               this(label, target, null);
+       }
+
+       public CmsLink(String label, String target, String custom) {
+               super();
+               this.label = label;
+               this.target = target;
+               this.custom = custom;
+               init();
+       }
+
+       public void init() {
+               if (image != null) {
+                       ImageData image = loadImage();
+                       if (imageHeight == null && imageWidth == null) {
+                               imageWidth = image.width;
+                               imageHeight = image.height;
+                       } else if (imageHeight == null) {
+                               imageHeight = (imageWidth * image.height) / image.width;
+                       } else if (imageWidth == null) {
+                               imageWidth = (imageHeight * image.width) / image.height;
+                       }
+               }
+       }
+
+       /** @return {@link Composite} with a single {@link Label} child. */
+       @Override
+       public Control createUi(final Composite parent, Node context) {
+//             if (image != null && (imageWidth == null || imageHeight == null)) {
+//                     throw new CmsException("Image is not properly configured."
+//                                     + " Make sure bundleContext property is set and init() method has been called.");
+//             }
+
+               Composite comp = new Composite(parent, SWT.BOTTOM);
+               comp.setLayout(CmsUtils.noSpaceGridLayout());
+
+               Label link = new Label(comp, SWT.NONE);
+               link.setData(RWT.MARKUP_ENABLED, Boolean.TRUE);
+               GridData layoutData = new GridData(horizontalAlignment, verticalAlignment, false, false);
+               if (image != null) {
+                       if (imageHeight != null)
+                               layoutData.heightHint = imageHeight;
+                       if (label == null)
+                               if (imageWidth != null)
+                                       layoutData.widthHint = imageWidth;
+               }
+
+               link.setLayoutData(layoutData);
+               if (custom != null) {
+                       comp.setData(RWT.CUSTOM_VARIANT, custom);
+                       link.setData(RWT.CUSTOM_VARIANT, custom);
+               } else {
+                       comp.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_LINK);
+                       link.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_LINK);
+               }
+
+               // label
+               StringBuilder labelText = new StringBuilder();
+               if (loggedInTarget != null && isLoggedIn()) {
+                       labelText.append("<a style='color:inherit;text-decoration:inherit;' href=\"");
+                       if (loggedInTarget.equals("")) {
+                               try {
+                                       Node homeNode = NodeUtils.getUserHome(context.getSession());
+                                       String homePath = homeNode.getPath();
+                                       labelText.append("/#" + homePath);
+                               } catch (RepositoryException e) {
+                                       throw new CmsException("Cannot get home path", e);
+                               }
+                       } else {
+                               labelText.append(loggedInTarget);
+                       }
+                       labelText.append("\">");
+               } else if (target != null) {
+                       labelText.append("<a style='color:inherit;text-decoration:inherit;' href=\"");
+                       labelText.append(target);
+                       labelText.append("\">");
+               }
+               if (image != null) {
+                       registerImageIfNeeded();
+                       String imageLocation = RWT.getResourceManager().getLocation(image);
+                       labelText.append("<img");
+                       if (imageWidth != null)
+                               labelText.append(" width='").append(imageWidth).append('\'');
+                       if (imageHeight != null)
+                               labelText.append(" height='").append(imageHeight).append('\'');
+                       labelText.append(" src=\"").append(imageLocation).append("\"/>");
+
+               }
+
+               if (loggedInLabel != null && isLoggedIn()) {
+                       labelText.append(' ').append(loggedInLabel);
+               } else if (label != null) {
+                       labelText.append(' ').append(label);
+               }
+
+               if ((loggedInTarget != null && isLoggedIn()) || target != null)
+                       labelText.append("</a>");
+
+               link.setText(labelText.toString());
+
+               if (mouseListener != null)
+                       link.addMouseListener(mouseListener);
+
+               return comp;
+       }
+
+       private void registerImageIfNeeded() {
+               ResourceManager resourceManager = RWT.getResourceManager();
+               if (!resourceManager.isRegistered(image)) {
+                       URL res = getImageUrl();
+                       InputStream inputStream = null;
+                       try {
+                               IOUtils.closeQuietly(inputStream);
+                               inputStream = res.openStream();
+                               resourceManager.register(image, inputStream);
+                               if (log.isTraceEnabled())
+                                       log.trace("Registered image " + image);
+                       } catch (Exception e) {
+                               throw new CmsException("Cannot load image " + image, e);
+                       } finally {
+                               IOUtils.closeQuietly(inputStream);
+                       }
+               }
+       }
+
+       private ImageData loadImage() {
+               URL url = getImageUrl();
+               ImageData result = null;
+               InputStream inputStream = null;
+               try {
+                       inputStream = url.openStream();
+                       result = new ImageData(inputStream);
+                       if (log.isTraceEnabled())
+                               log.trace("Loaded image " + image);
+               } catch (Exception e) {
+                       throw new CmsException("Cannot load image " + image, e);
+               } finally {
+                       IOUtils.closeQuietly(inputStream);
+               }
+               return result;
+       }
+
+       private URL getImageUrl() {
+               URL url;
+               try {
+                       // pure URL
+                       url = new URL(image);
+               } catch (MalformedURLException e1) {
+                       url = bundleContext.getBundle().getResource(image);
+               }
+
+               if (url == null)
+                       throw new CmsException("No image " + image + " available.");
+
+               return url;
+       }
+
+       public void setBundleContext(BundleContext bundleContext) {
+               this.bundleContext = bundleContext;
+       }
+
+       public void setLabel(String label) {
+               this.label = label;
+       }
+
+       public void setCustom(String custom) {
+               this.custom = custom;
+       }
+
+       public void setTarget(String target) {
+               this.target = target;
+               // try {
+               // new URL(target);
+               // isUrl = true;
+               // } catch (MalformedURLException e1) {
+               // isUrl = false;
+               // }
+       }
+
+       public void setImage(String image) {
+               this.image = image;
+       }
+
+       public void setLoggedInLabel(String loggedInLabel) {
+               this.loggedInLabel = loggedInLabel;
+       }
+
+       public void setLoggedInTarget(String loggedInTarget) {
+               this.loggedInTarget = loggedInTarget;
+       }
+
+       public void setMouseListener(MouseListener mouseListener) {
+               this.mouseListener = mouseListener;
+       }
+
+       public void setvAlign(String vAlign) {
+               if ("bottom".equals(vAlign)) {
+                       verticalAlignment = SWT.BOTTOM;
+               } else if ("top".equals(vAlign)) {
+                       verticalAlignment = SWT.TOP;
+               } else if ("center".equals(vAlign)) {
+                       verticalAlignment = SWT.CENTER;
+               } else {
+                       throw new CmsException("Unsupported vertical allignment " + vAlign + " (must be: top, bottom or center)");
+               }
+       }
+
+       protected boolean isLoggedIn() {
+               return !CurrentUser.isAnonymous();
+       }
+
+       public void setImageWidth(Integer imageWidth) {
+               this.imageWidth = imageWidth;
+       }
+
+       public void setImageHeight(Integer imageHeight) {
+               this.imageHeight = imageHeight;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/CmsPane.java b/org.argeo.cms.ui/src/org/argeo/cms/util/CmsPane.java
new file mode 100644 (file)
index 0000000..a8d085c
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.cms.util;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
+import org.eclipse.swt.widgets.Composite;
+
+/** The main pane of a CMS display, with QA and support areas. */
+public class CmsPane {
+
+       private Composite mainArea;
+       private Composite qaArea;
+       private Composite supportArea;
+
+       public CmsPane(Composite parent, int style) {
+               parent.setLayout(CmsUtils.noSpaceGridLayout());
+
+//             qaArea = new Composite(parent, SWT.NONE);
+//             qaArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+//             RowLayout qaLayout = new RowLayout();
+//             qaLayout.spacing = 0;
+//             qaArea.setLayout(qaLayout);
+
+               mainArea = new Composite(parent, SWT.NONE);
+               mainArea.setLayout(new GridLayout());
+               mainArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+//             supportArea = new Composite(parent, SWT.NONE);
+//             supportArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+//             RowLayout supportLayout = new RowLayout();
+//             supportLayout.spacing = 0;
+//             supportArea.setLayout(supportLayout);
+       }
+
+       public Composite getMainArea() {
+               return mainArea;
+       }
+
+       public Composite getQaArea() {
+               return qaArea;
+       }
+
+       public Composite getSupportArea() {
+               return supportArea;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/CmsUtils.java b/org.argeo.cms.ui/src/org/argeo/cms/util/CmsUtils.java
new file mode 100644 (file)
index 0000000..7436915
--- /dev/null
@@ -0,0 +1,219 @@
+package org.argeo.cms.util;
+
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.CmsConstants;
+import org.argeo.cms.ui.CmsView;
+import org.argeo.eclipse.ui.specific.UiContext;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeUtils;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.service.ResourceManager;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Widget;
+
+/** Static utilities for the CMS framework. */
+public class CmsUtils implements CmsConstants {
+       // private final static Log log = LogFactory.getLog(CmsUtils.class);
+
+       /**
+        * The CMS view related to this display, or null if none is available from this
+        * call.
+        */
+       public static CmsView getCmsView() {
+               return UiContext.getData(CmsView.KEY);
+       }
+
+       public static StringBuilder getServerBaseUrl(HttpServletRequest request) {
+               try {
+                       URL url = new URL(request.getRequestURL().toString());
+                       StringBuilder buf = new StringBuilder();
+                       buf.append(url.getProtocol()).append("://").append(url.getHost());
+                       if (url.getPort() != -1)
+                               buf.append(':').append(url.getPort());
+                       return buf;
+               } catch (MalformedURLException e) {
+                       throw new CmsException("Cannot extract server base URL from " + request.getRequestURL(), e);
+               }
+       }
+
+       //
+       public static String getDataUrl(Node node, HttpServletRequest request) throws RepositoryException {
+               try {
+                       StringBuilder buf = getServerBaseUrl(request);
+                       buf.append(getDataPath(node));
+                       return new URL(buf.toString()).toString();
+               } catch (MalformedURLException e) {
+                       throw new CmsException("Cannot build data URL for " + node, e);
+               }
+       }
+
+       /** A path in the node repository */
+       public static String getDataPath(Node node) throws RepositoryException {
+               return getDataPath(NodeConstants.NODE, node);
+       }
+
+       public static String getDataPath(String cn, Node node) throws RepositoryException {
+               return NodeUtils.getDataPath(cn, node);
+       }
+
+       /** @deprecated Use rowData16px() instead. GridData should not be reused. */
+       @Deprecated
+       public static RowData ROW_DATA_16px = new RowData(16, 16);
+
+       public static GridLayout noSpaceGridLayout() {
+               return noSpaceGridLayout(new GridLayout());
+       }
+
+       public static GridLayout noSpaceGridLayout(int columns) {
+               return noSpaceGridLayout(new GridLayout(columns, false));
+       }
+
+       public static GridLayout noSpaceGridLayout(GridLayout layout) {
+               layout.horizontalSpacing = 0;
+               layout.verticalSpacing = 0;
+               layout.marginWidth = 0;
+               layout.marginHeight = 0;
+               return layout;
+       }
+
+       //
+       // GRID DATA
+       //
+       public static GridData fillWidth() {
+               return grabWidth(SWT.FILL, SWT.FILL);
+       }
+
+       public static GridData fillAll() {
+               return new GridData(SWT.FILL, SWT.FILL, true, true);
+       }
+
+       public static GridData grabWidth(int horizontalAlignment, int verticalAlignment) {
+               return new GridData(horizontalAlignment, horizontalAlignment, true, false);
+       }
+
+       public static RowData rowData16px() {
+               return new RowData(16, 16);
+       }
+
+       /** Style widget */
+       public static void style(Widget widget, String style) {
+               widget.setData(CmsConstants.STYLE, style);
+       }
+
+       /** Enable markups on widget */
+       public static void markup(Widget widget) {
+               widget.setData(CmsConstants.MARKUP, true);
+       }
+
+       public static void setItemHeight(Table table, int height) {
+               table.setData(CmsConstants.ITEM_HEIGHT, height);
+       }
+
+       /** Dispose all children of a Composite */
+       public static void clear(Composite composite) {
+               for (Control child : composite.getChildren())
+                       child.dispose();
+       }
+
+       //
+       // JCR
+       //
+       public static Node getOrAddEmptyFile(Node parent, Enum<?> child) throws RepositoryException {
+               if (has(parent, child))
+                       return child(parent, child);
+               return JcrUtils.copyBytesAsFile(parent, child.name(), new byte[0]);
+       }
+
+       public static Node child(Node parent, Enum<?> en) throws RepositoryException {
+               return parent.getNode(en.name());
+       }
+
+       public static Boolean has(Node parent, Enum<?> en) throws RepositoryException {
+               return parent.hasNode(en.name());
+       }
+
+       public static Node getOrAdd(Node parent, Enum<?> en) throws RepositoryException {
+               return getOrAdd(parent, en, null);
+       }
+
+       public static Node getOrAdd(Node parent, Enum<?> en, String primaryType) throws RepositoryException {
+               if (has(parent, en))
+                       return child(parent, en);
+               else if (primaryType == null)
+                       return parent.addNode(en.name());
+               else
+                       return parent.addNode(en.name(), primaryType);
+       }
+
+       // IMAGES
+       public static String img(String src, String width, String height) {
+               return imgBuilder(src, width, height).append("/>").toString();
+       }
+
+       public static String img(String src, Point size) {
+               return img(src, Integer.toString(size.x), Integer.toString(size.y));
+       }
+
+       public static StringBuilder imgBuilder(String src, String width, String height) {
+               return new StringBuilder(64).append("<img width='").append(width).append("' height='").append(height)
+                               .append("' src='").append(src).append("'");
+       }
+
+       public static String noImg(Point size) {
+               ResourceManager rm = RWT.getResourceManager();
+               return CmsUtils.img(rm.getLocation(NO_IMAGE), size);
+       }
+
+       public static String noImg() {
+               return noImg(NO_IMAGE_SIZE);
+       }
+
+       public static Image noImage(Point size) {
+               ResourceManager rm = RWT.getResourceManager();
+               InputStream in = null;
+               try {
+                       in = rm.getRegisteredContent(NO_IMAGE);
+                       ImageData id = new ImageData(in);
+                       ImageData scaled = id.scaledTo(size.x, size.y);
+                       Image image = new Image(Display.getCurrent(), scaled);
+                       return image;
+               } finally {
+                       IOUtils.closeQuietly(in);
+               }
+       }
+
+       public final static String LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
+                       + " Etiam eleifend hendrerit sem, ac ultricies massa ornare ac."
+                       + " Cras aliquam sodales risus, vitae varius lacus molestie quis."
+                       + " Vivamus consequat, leo id lacinia volutpat, eros diam efficitur urna, finibus interdum risus turpis at nisi."
+                       + " Curabitur vulputate nulla quis scelerisque fringilla. Integer consectetur turpis id lobortis accumsan."
+                       + " Pellentesque commodo turpis ac diam ultricies dignissim."
+                       + " Curabitur sit amet dolor volutpat lacus aliquam ornare quis sed velit."
+                       + " Integer varius quis est et tristique."
+                       + " Suspendisse pharetra porttitor purus, eget condimentum magna."
+                       + " Duis vitae turpis eros. Sed tincidunt lacinia rutrum."
+                       + " Aliquam velit velit, rutrum ut augue sed, condimentum lacinia augue.";
+
+       private CmsUtils() {
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/LoginEntryPoint.java b/org.argeo.cms.ui/src/org/argeo/cms/util/LoginEntryPoint.java
new file mode 100644 (file)
index 0000000..b7bf910
--- /dev/null
@@ -0,0 +1,181 @@
+package org.argeo.cms.util;
+
+import java.util.Locale;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.CmsImageManager;
+import org.argeo.cms.ui.CmsView;
+import org.argeo.cms.ui.UxContext;
+import org.argeo.cms.widgets.auth.CmsLogin;
+import org.argeo.cms.widgets.auth.CmsLoginShell;
+import org.argeo.eclipse.ui.specific.UiContext;
+import org.argeo.node.NodeConstants;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.application.EntryPoint;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+
+public class LoginEntryPoint implements EntryPoint, CmsView {
+       protected final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
+       protected final static String HEADER_AUTHORIZATION = "Authorization";
+       private final static Log log = LogFactory.getLog(LoginEntryPoint.class);
+       private LoginContext loginContext;
+       private UxContext uxContext = null;
+
+       @Override
+       public int createUI() {
+               final Display display = createDisplay();
+               UiContext.setData(CmsView.KEY, this);
+               CmsLoginShell loginShell = createCmsLoginShell();
+               try {
+                       // try pre-auth
+                       loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, loginShell);
+                       loginContext.login();
+               } catch (LoginException e) {
+                       loginShell.createUi();
+                       loginShell.open();
+
+                       // HttpServletRequest request = RWT.getRequest();
+                       // String authorization = request.getHeader(HEADER_AUTHORIZATION);
+                       // if (authorization == null ||
+                       // !authorization.startsWith("Negotiate")) {
+                       // HttpServletResponse response = RWT.getResponse();
+                       // response.setStatus(401);
+                       // response.setHeader(HEADER_WWW_AUTHENTICATE, "Negotiate");
+                       // response.setDateHeader("Date", System.currentTimeMillis());
+                       // response.setDateHeader("Expires", System.currentTimeMillis() +
+                       // (24 * 60 * 60 * 1000));
+                       // response.setHeader("Accept-Ranges", "bytes");
+                       // response.setHeader("Connection", "Keep-Alive");
+                       // response.setHeader("Keep-Alive", "timeout=5, max=97");
+                       // // response.setContentType("text/html; charset=UTF-8");
+                       // }
+
+                       while (!loginShell.getShell().isDisposed()) {
+                               if (!display.readAndDispatch())
+                                       display.sleep();
+                       }
+               }
+
+               if (CurrentUser.getUsername(getSubject()) == null)
+                       return -1;
+               uxContext = new SimpleUxContext();
+               return postLogin();
+       }
+
+       protected Display createDisplay() {
+               return new Display();
+       }
+
+       protected int postLogin() {
+               return 0;
+       }
+
+       protected HttpServletRequest getRequest() {
+               return RWT.getRequest();
+       }
+
+       protected CmsLoginShell createCmsLoginShell() {
+               return new CmsLoginShell(this) {
+
+                       @Override
+                       public void createContents(Composite parent) {
+                               LoginEntryPoint.this.createLoginPage(parent, this);
+                       }
+
+                       @Override
+                       protected void extendsCredentialsBlock(Composite credentialsBlock, Locale selectedLocale,
+                                       SelectionListener loginSelectionListener) {
+                               LoginEntryPoint.this.extendsCredentialsBlock(credentialsBlock, selectedLocale, loginSelectionListener);
+                       }
+
+               };
+       }
+
+       /**
+        * To be overridden. CmsLogin#createCredentialsBlock() should be called at
+        * some point in order to create the credentials composite. In order to use
+        * the default layout, call CmsLogin#defaultCreateContents() but <b>not</b>
+        * CmsLogin#createContent(), since it would lead to a stack overflow.
+        */
+       protected void createLoginPage(Composite parent, CmsLogin login) {
+               login.defaultCreateContents(parent);
+       }
+
+       protected void extendsCredentialsBlock(Composite credentialsBlock, Locale selectedLocale,
+                       SelectionListener loginSelectionListener) {
+
+       }
+
+       @Override
+       public void navigateTo(String state) {
+               // TODO Auto-generated method stub
+
+       }
+
+       @Override
+       public void authChange(LoginContext loginContext) {
+               if (loginContext == null)
+                       throw new CmsException("Login context cannot be null");
+               // logout previous login context
+               if (this.loginContext != null)
+                       try {
+                               this.loginContext.logout();
+                       } catch (LoginException e1) {
+                               log.warn("Could not log out: " + e1);
+                       }
+               this.loginContext = loginContext;
+       }
+
+       @Override
+       public void logout() {
+               if (loginContext == null)
+                       throw new CmsException("Login context should not bet null");
+               try {
+                       CurrentUser.logoutCmsSession(loginContext.getSubject());
+                       loginContext.logout();
+               } catch (LoginException e) {
+                       throw new CmsException("Cannot log out", e);
+               }
+       }
+
+       @Override
+       public void exception(Throwable e) {
+               // TODO Auto-generated method stub
+
+       }
+
+       // @Override
+       // public LoginContext getLoginContext() {
+       // return loginContext;
+       // }
+
+       protected Subject getSubject() {
+               return loginContext.getSubject();
+       }
+
+       @Override
+       public boolean isAnonymous() {
+               return CurrentUser.isAnonymous(getSubject());
+       }
+
+       @Override
+       public CmsImageManager getImageManager() {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       @Override
+       public UxContext getUxContext() {
+               return uxContext;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/MenuLink.java b/org.argeo.cms.ui/src/org/argeo/cms/util/MenuLink.java
new file mode 100644 (file)
index 0000000..79fd7cb
--- /dev/null
@@ -0,0 +1,22 @@
+package org.argeo.cms.util;
+
+import org.argeo.cms.ui.CmsStyles;
+
+/**
+ * Convenience class setting the custom style {@link CmsStyles#CMS_MENU_LINK} on
+ * a {@link CmsLink} when simple menus are used.
+ */
+public class MenuLink extends CmsLink {
+       public MenuLink() {
+               setCustom(CmsStyles.CMS_MENU_LINK);
+       }
+
+       public MenuLink(String label, String target, String custom) {
+               super(label, target, custom);
+       }
+
+       public MenuLink(String label, String target) {
+               super(label, target, CmsStyles.CMS_MENU_LINK);
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleApp.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleApp.java
new file mode 100644 (file)
index 0000000..5b0e1b7
--- /dev/null
@@ -0,0 +1,373 @@
+package org.argeo.cms.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.security.Privilege;
+import javax.jcr.version.VersionManager;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.CmsConstants;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.cms.ui.LifeCycleUiProvider;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.NodeConstants;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.application.Application;
+import org.eclipse.rap.rwt.application.Application.OperationMode;
+import org.eclipse.rap.rwt.application.ApplicationConfiguration;
+import org.eclipse.rap.rwt.application.EntryPoint;
+import org.eclipse.rap.rwt.application.EntryPointFactory;
+import org.eclipse.rap.rwt.application.ExceptionHandler;
+import org.eclipse.rap.rwt.client.WebClient;
+import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
+import org.eclipse.rap.rwt.service.ResourceLoader;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+/** A basic generic app based on {@link SimpleErgonomics}. */
+public class SimpleApp implements CmsConstants, ApplicationConfiguration {
+       private final static Log log = LogFactory.getLog(SimpleApp.class);
+
+       private String contextName = null;
+
+       private Map<String, Map<String, String>> branding = new HashMap<String, Map<String, String>>();
+       private Map<String, List<String>> styleSheets = new HashMap<String, List<String>>();
+
+       private List<String> resources = new ArrayList<String>();
+
+       private BundleContext bundleContext;
+
+       private Repository repository;
+       private String workspace = null;
+       private String jcrBasePath = "/";
+       private List<String> roPrincipals = Arrays.asList(NodeConstants.ROLE_ANONYMOUS, NodeConstants.ROLE_USER);
+       private List<String> rwPrincipals = Arrays.asList(NodeConstants.ROLE_USER);
+
+       private CmsUiProvider header;
+       private Map<String, CmsUiProvider> pages = new LinkedHashMap<String, CmsUiProvider>();
+
+       private Integer headerHeight = 40;
+
+       private ServiceRegistration<ApplicationConfiguration> appReg;
+
+       public void configure(Application application) {
+               try {
+                       BundleResourceLoader bundleRL = new BundleResourceLoader(bundleContext.getBundle());
+
+                       application.setOperationMode(OperationMode.SWT_COMPATIBILITY);
+                       // application.setOperationMode(OperationMode.JEE_COMPATIBILITY);
+
+                       application.setExceptionHandler(new CmsExceptionHandler());
+
+                       // loading animated gif
+                       application.addResource(LOADING_IMAGE, createResourceLoader(LOADING_IMAGE));
+                       // empty image
+                       application.addResource(NO_IMAGE, createResourceLoader(NO_IMAGE));
+
+                       for (String resource : resources) {
+                               application.addResource(resource, bundleRL);
+                               if (log.isTraceEnabled())
+                                       log.trace("Resource " + resource);
+                       }
+
+                       Map<String, String> defaultBranding = null;
+                       if (branding.containsKey("*"))
+                               defaultBranding = branding.get("*");
+                       String defaultTheme = defaultBranding.get(WebClient.THEME_ID);
+
+                       // entry points
+                       for (String page : pages.keySet()) {
+                               Map<String, String> properties = defaultBranding != null ? new HashMap<String, String>(defaultBranding)
+                                               : new HashMap<String, String>();
+                               if (branding.containsKey(page)) {
+                                       properties.putAll(branding.get(page));
+                               }
+                               // favicon
+                               if (properties.containsKey(WebClient.FAVICON)) {
+                                       String themeId = defaultBranding.get(WebClient.THEME_ID);
+                                       Bundle themeBundle = ThemeUtils.findThemeBundle(bundleContext, themeId);
+                                       String faviconRelPath = properties.get(WebClient.FAVICON);
+                                       application.addResource(faviconRelPath,
+                                                       new BundleResourceLoader(themeBundle != null ? themeBundle : bundleContext.getBundle()));
+                                       if (log.isTraceEnabled())
+                                               log.trace("Favicon " + faviconRelPath);
+
+                               }
+
+                               // page title
+                               if (!properties.containsKey(WebClient.PAGE_TITLE)) {
+                                       if (page.length() > 0)
+                                               properties.put(WebClient.PAGE_TITLE, Character.toUpperCase(page.charAt(0)) + page.substring(1));
+                               }
+
+                               // default body HTML
+                               if (!properties.containsKey(WebClient.BODY_HTML))
+                                       properties.put(WebClient.BODY_HTML, DEFAULT_LOADING_BODY);
+
+                               //
+                               // ADD ENTRY POINT
+                               //
+                               application.addEntryPoint("/" + page,
+                                               new CmsEntryPointFactory(pages.get(page), repository, workspace, properties), properties);
+                               log.info("Page /" + page);
+                       }
+
+                       // stylesheets and themes
+                       Set<Bundle> themeBundles = new HashSet<>();
+                       for (String themeId : styleSheets.keySet()) {
+                               Bundle themeBundle = ThemeUtils.findThemeBundle(bundleContext, themeId);
+                               StyleSheetResourceLoader styleSheetRL = new StyleSheetResourceLoader(
+                                               themeBundle != null ? themeBundle : bundleContext.getBundle());
+                               if (themeBundle != null)
+                                       themeBundles.add(themeBundle);
+                               List<String> cssLst = styleSheets.get(themeId);
+                               if (log.isDebugEnabled())
+                                       log.debug("Theme " + themeId);
+                               for (String css : cssLst) {
+                                       application.addStyleSheet(themeId, css, styleSheetRL);
+                                       if (log.isTraceEnabled())
+                                               log.trace(" CSS " + css);
+                               }
+
+                       }
+                       for (Bundle themeBundle : themeBundles) {
+                               BundleResourceLoader themeBRL = new BundleResourceLoader(themeBundle);
+                               ThemeUtils.addThemeResources(application, themeBundle, themeBRL, "*.png");
+                               ThemeUtils.addThemeResources(application, themeBundle, themeBRL, "*.gif");
+                               ThemeUtils.addThemeResources(application, themeBundle, themeBRL, "*.jpg");
+                       }
+               } catch (RuntimeException e) {
+                       // Easier access to initialisation errors
+                       log.error("Unexpected exception when configuring RWT application.", e);
+                       throw e;
+               }
+       }
+
+       public void init() throws RepositoryException {
+               Session session = null;
+               try {
+                       session = JcrUtils.loginOrCreateWorkspace(repository, workspace);
+                       VersionManager vm = session.getWorkspace().getVersionManager();
+                       JcrUtils.mkdirs(session, jcrBasePath);
+                       session.save();
+                       if (!vm.isCheckedOut(jcrBasePath))
+                               vm.checkout(jcrBasePath);
+                       for (String principal : rwPrincipals)
+                               JcrUtils.addPrivilege(session, jcrBasePath, principal, Privilege.JCR_WRITE);
+                       for (String principal : roPrincipals)
+                               JcrUtils.addPrivilege(session, jcrBasePath, principal, Privilege.JCR_READ);
+
+                       for (String pageName : pages.keySet()) {
+                               try {
+                                       initPage(session, pages.get(pageName));
+                                       session.save();
+                               } catch (Exception e) {
+                                       throw new CmsException("Cannot initialize page " + pageName, e);
+                               }
+                       }
+
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+
+               // publish to OSGi
+               register();
+       }
+
+       protected void initPage(Session adminSession, CmsUiProvider page) throws RepositoryException {
+               if (page instanceof LifeCycleUiProvider)
+                       ((LifeCycleUiProvider) page).init(adminSession);
+       }
+
+       public void destroy() {
+               for (String pageName : pages.keySet()) {
+                       try {
+                               CmsUiProvider page = pages.get(pageName);
+                               if (page instanceof LifeCycleUiProvider)
+                                       ((LifeCycleUiProvider) page).destroy();
+                       } catch (Exception e) {
+                               log.error("Cannot destroy page " + pageName, e);
+                       }
+               }
+       }
+
+       protected void register() {
+               Hashtable<String, String> props = new Hashtable<String, String>();
+               if (contextName != null)
+                       props.put("contextName", contextName);
+               appReg = bundleContext.registerService(ApplicationConfiguration.class, this, props);
+               if (log.isDebugEnabled())
+                       log.debug("Registered " + (contextName == null ? "/" : contextName));
+       }
+
+       protected void unregister() {
+               appReg.unregister();
+               if (log.isDebugEnabled())
+                       log.debug("Unregistered " + (contextName == null ? "/" : contextName));
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setWorkspace(String workspace) {
+               this.workspace = workspace;
+       }
+
+       public void setHeader(CmsUiProvider header) {
+               this.header = header;
+       }
+
+       public void setPages(Map<String, CmsUiProvider> pages) {
+               this.pages = pages;
+       }
+
+       public void setJcrBasePath(String basePath) {
+               this.jcrBasePath = basePath;
+       }
+
+       public void setRoPrincipals(List<String> roPrincipals) {
+               this.roPrincipals = roPrincipals;
+       }
+
+       public void setRwPrincipals(List<String> rwPrincipals) {
+               this.rwPrincipals = rwPrincipals;
+       }
+
+       public void setHeaderHeight(Integer headerHeight) {
+               this.headerHeight = headerHeight;
+       }
+
+       public void setBranding(Map<String, Map<String, String>> branding) {
+               this.branding = branding;
+       }
+
+       public void setStyleSheets(Map<String, List<String>> styleSheets) {
+               this.styleSheets = styleSheets;
+       }
+
+       public void setBundleContext(BundleContext bundleContext) {
+               this.bundleContext = bundleContext;
+       }
+
+       public void setResources(List<String> resources) {
+               this.resources = resources;
+       }
+
+       public void setContextName(String contextName) {
+               this.contextName = contextName;
+       }
+
+       class CmsExceptionHandler implements ExceptionHandler {
+
+               @Override
+               public void handleException(Throwable throwable) {
+                       // TODO be smarter
+                       CmsUtils.getCmsView().exception(throwable);
+               }
+
+       }
+
+       private class CmsEntryPointFactory implements EntryPointFactory {
+               private final CmsUiProvider page;
+               private final Repository repository;
+               private final String workspace;
+               private final Map<String, String> properties;
+
+               public CmsEntryPointFactory(CmsUiProvider page, Repository repository, String workspace,
+                               Map<String, String> properties) {
+                       this.page = page;
+                       this.repository = repository;
+                       this.workspace = workspace;
+                       this.properties = properties;
+               }
+
+               @Override
+               public EntryPoint create() {
+                       SimpleErgonomics entryPoint = new SimpleErgonomics(repository, workspace, jcrBasePath, page, properties) {
+                               private static final long serialVersionUID = -637940404865527290L;
+
+                               @Override
+                               protected void createAdminArea(Composite parent) {
+                                       Composite adminArea = new Composite(parent, SWT.NONE);
+                                       adminArea.setLayout(new FillLayout());
+                                       Button refresh = new Button(adminArea, SWT.PUSH);
+                                       refresh.setText("Reload App");
+                                       refresh.addSelectionListener(new SelectionAdapter() {
+                                               private static final long serialVersionUID = -7671999525536351366L;
+
+                                               @Override
+                                               public void widgetSelected(SelectionEvent e) {
+                                                       long timeBeforeReload = 1000;
+                                                       RWT.getClient().getService(JavaScriptExecutor.class).execute(
+                                                                       "setTimeout(function() { " + "location.reload();" + "}," + timeBeforeReload + ");");
+                                                       reloadApp();
+                                               }
+                                       });
+                               }
+                       };
+                       // entryPoint.setState("");
+                       entryPoint.setHeader(header);
+                       entryPoint.setHeaderHeight(headerHeight);
+                       // CmsSession.current.set(entryPoint);
+                       return entryPoint;
+               }
+
+               private void reloadApp() {
+                       new Thread("Refresh app") {
+                               @Override
+                               public void run() {
+                                       unregister();
+                                       register();
+                               }
+                       }.start();
+               }
+       }
+
+       private static ResourceLoader createResourceLoader(final String resourceName) {
+               return new ResourceLoader() {
+                       public InputStream getResourceAsStream(String resourceName) throws IOException {
+                               return getClass().getClassLoader().getResourceAsStream(resourceName);
+                       }
+               };
+       }
+
+       // private static ResourceLoader createUrlResourceLoader(final URL url) {
+       // return new ResourceLoader() {
+       // public InputStream getResourceAsStream(String resourceName)
+       // throws IOException {
+       // return url.openStream();
+       // }
+       // };
+       // }
+
+       /*
+        * TEXTS
+        */
+       private static String DEFAULT_LOADING_BODY = "<div"
+                       + " style=\"position: absolute; left: 50%; top: 50%; margin: -32px -32px; width: 64px; height:64px\">"
+                       + "<img src=\"./rwt-resources/" + LOADING_IMAGE
+                       + "\" width=\"32\" height=\"32\" style=\"margin: 16px 16px\"/>" + "</div>";
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleCmsHeader.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleCmsHeader.java
new file mode 100644 (file)
index 0000000..ae1299d
--- /dev/null
@@ -0,0 +1,96 @@
+package org.argeo.cms.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.CmsStyles;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/** A header in three parts */
+public class SimpleCmsHeader implements CmsUiProvider {
+       private List<CmsUiProvider> lead = new ArrayList<CmsUiProvider>();
+       private List<CmsUiProvider> center = new ArrayList<CmsUiProvider>();
+       private List<CmsUiProvider> end = new ArrayList<CmsUiProvider>();
+
+       private Boolean subPartsSameWidth = false;
+
+       @Override
+       public Control createUi(Composite parent, Node context) throws RepositoryException {
+               Composite header = new Composite(parent, SWT.NONE);
+               header.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_HEADER);
+               header.setBackgroundMode(SWT.INHERIT_DEFAULT);
+               header.setLayout(CmsUtils.noSpaceGridLayout(new GridLayout(3, false)));
+
+               configurePart(context, header, lead);
+               configurePart(context, header, center);
+               configurePart(context, header, end);
+               return header;
+       }
+
+       protected void configurePart(Node context, Composite parent, List<CmsUiProvider> partProviders)
+                       throws RepositoryException {
+               final int style;
+               final String custom;
+               if (lead == partProviders) {
+                       style = SWT.LEAD;
+                       custom = CmsStyles.CMS_HEADER_LEAD;
+               } else if (center == partProviders) {
+                       style = SWT.CENTER;
+                       custom = CmsStyles.CMS_HEADER_CENTER;
+               } else if (end == partProviders) {
+                       style = SWT.END;
+                       custom = CmsStyles.CMS_HEADER_END;
+               } else {
+                       throw new CmsException("Unsupported part providers " + partProviders);
+               }
+
+               Composite part = new Composite(parent, SWT.NONE);
+               part.setData(RWT.CUSTOM_VARIANT, custom);
+               GridData gridData = new GridData(style, SWT.FILL, true, true);
+               part.setLayoutData(gridData);
+               part.setLayout(CmsUtils.noSpaceGridLayout(new GridLayout(partProviders.size(), subPartsSameWidth)));
+               for (CmsUiProvider uiProvider : partProviders) {
+                       Control subPart = uiProvider.createUi(part, context);
+                       subPart.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               }
+       }
+
+       public void setLead(List<CmsUiProvider> lead) {
+               this.lead = lead;
+       }
+
+       public void setCenter(List<CmsUiProvider> center) {
+               this.center = center;
+       }
+
+       public void setEnd(List<CmsUiProvider> end) {
+               this.end = end;
+       }
+
+       public void setSubPartsSameWidth(Boolean subPartsSameWidth) {
+               this.subPartsSameWidth = subPartsSameWidth;
+       }
+
+       public List<CmsUiProvider> getLead() {
+               return lead;
+       }
+
+       public List<CmsUiProvider> getCenter() {
+               return center;
+       }
+
+       public List<CmsUiProvider> getEnd() {
+               return end;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleDynamicPages.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleDynamicPages.java
new file mode 100644 (file)
index 0000000..dd95e7f
--- /dev/null
@@ -0,0 +1,118 @@
+package org.argeo.cms.util;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.jcr.JcrUtils;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+
+public class SimpleDynamicPages implements CmsUiProvider {
+
+       @Override
+       public Control createUi(Composite parent, Node context)
+                       throws RepositoryException {
+               if (context == null)
+                       throw new CmsException("Context cannot be null");
+               parent.setLayout(new GridLayout(2, false));
+
+               // parent
+               if (!context.getPath().equals("/")) {
+                       new CmsLink("..", context.getParent().getPath()).createUi(parent,
+                                       context);
+                       new Label(parent, SWT.NONE).setText(context.getParent()
+                                       .getPrimaryNodeType().getName());
+               }
+
+               // context
+               Label contextL = new Label(parent, SWT.NONE);
+               contextL.setData(RWT.MARKUP_ENABLED, true);
+               contextL.setText("<b>" + context.getName() + "</b>");
+               new Label(parent, SWT.NONE).setText(context.getPrimaryNodeType()
+                               .getName());
+
+               // children
+               // Label childrenL = new Label(parent, SWT.NONE);
+               // childrenL.setData(RWT.MARKUP_ENABLED, true);
+               // childrenL.setText("<i>Children:</i>");
+               // childrenL.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false,
+               // false, 2, 1));
+
+               for (NodeIterator nIt = context.getNodes(); nIt.hasNext();) {
+                       Node child = nIt.nextNode();
+                       new CmsLink(child.getName(), child.getPath()).createUi(parent,
+                                       context);
+
+                       new Label(parent, SWT.NONE).setText(child.getPrimaryNodeType()
+                                       .getName());
+               }
+
+               // properties
+               // Label propsL = new Label(parent, SWT.NONE);
+               // propsL.setData(RWT.MARKUP_ENABLED, true);
+               // propsL.setText("<i>Properties:</i>");
+               // propsL.setLayoutData(new GridData(SWT.LEAD, SWT.CENTER, false, false,
+               // 2, 1));
+               for (PropertyIterator pIt = context.getProperties(); pIt.hasNext();) {
+                       Property property = pIt.nextProperty();
+
+                       Label label = new Label(parent, SWT.NONE);
+                       label.setText(property.getName());
+                       label.setToolTipText(JcrUtils
+                                       .getPropertyDefinitionAsString(property));
+
+                       new Label(parent, SWT.NONE).setText(getPropAsString(property));
+               }
+
+               return null;
+       }
+
+       private String getPropAsString(Property property)
+                       throws RepositoryException {
+               String result = "";
+               DateFormat timeFormatter = new SimpleDateFormat("");
+               if (property.isMultiple()) {
+                       result = getMultiAsString(property, ", ");
+               } else {
+                       Value value = property.getValue();
+                       if (value.getType() == PropertyType.BINARY)
+                               result = "<binary>";
+                       else if (value.getType() == PropertyType.DATE)
+                               result = timeFormatter.format(value.getDate().getTime());
+                       else
+                               result = value.getString();
+               }
+               return result;
+       }
+
+       private String getMultiAsString(Property property, String separator)
+                       throws RepositoryException {
+               if (separator == null)
+                       separator = "; ";
+               Value[] values = property.getValues();
+               StringBuilder builder = new StringBuilder();
+               for (Value val : values) {
+                       String currStr = val.getString();
+                       if (!"".equals(currStr.trim()))
+                               builder.append(currStr).append(separator);
+               }
+               if (builder.lastIndexOf(separator) >= 0)
+                       return builder.substring(0, builder.length() - separator.length());
+               else
+                       return builder.toString();
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleErgonomics.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleErgonomics.java
new file mode 100644 (file)
index 0000000..ca56360
--- /dev/null
@@ -0,0 +1,230 @@
+package org.argeo.cms.util;
+
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.AbstractCmsEntryPoint;
+import org.argeo.cms.ui.CmsImageManager;
+import org.argeo.cms.ui.CmsStyles;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.argeo.cms.ui.UxContext;
+import org.argeo.cms.ui.internal.ImageManagerImpl;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/** Simple header/body ergonomics. */
+public class SimpleErgonomics extends AbstractCmsEntryPoint {
+       private static final long serialVersionUID = 8743413921359548523L;
+
+       private final static Log log = LogFactory.getLog(SimpleErgonomics.class);
+
+       private boolean uiInitialized = false;
+       private Composite headerArea;
+       private Composite leftArea;
+       private Composite rightArea;
+       private Composite footerArea;
+       private Composite bodyArea;
+       private final CmsUiProvider uiProvider;
+
+       private CmsUiProvider header;
+       private Integer headerHeight = 0;
+       private Integer footerHeight = 0;
+       private CmsUiProvider lead;
+       private CmsUiProvider end;
+       private CmsUiProvider footer;
+
+       private CmsImageManager imageManager = new ImageManagerImpl();
+       private UxContext uxContext = null;
+
+       public SimpleErgonomics(Repository repository, String workspace, String defaultPath, CmsUiProvider uiProvider,
+                       Map<String, String> factoryProperties) {
+               super(repository, workspace, defaultPath, factoryProperties);
+               this.uiProvider = uiProvider;
+       }
+
+       @Override
+       protected void initUi(Composite parent) {
+               parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               parent.setLayout(CmsUtils.noSpaceGridLayout(new GridLayout(3, false)));
+
+               uxContext = new SimpleUxContext();
+               if (!getUxContext().isMasterData())
+                       createAdminArea(parent);
+               headerArea = new Composite(parent, SWT.NONE);
+               headerArea.setLayout(new FillLayout());
+               GridData headerData = new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1);
+               headerData.heightHint = headerHeight;
+               headerArea.setLayoutData(headerData);
+
+               // TODO: bi-directional
+               leftArea = new Composite(parent, SWT.NONE);
+               leftArea.setLayoutData(new GridData(SWT.LEAD, SWT.TOP, false, false));
+               leftArea.setLayout(CmsUtils.noSpaceGridLayout());
+
+               bodyArea = new Composite(parent, SWT.NONE);
+               bodyArea.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_BODY);
+               bodyArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               bodyArea.setLayout(CmsUtils.noSpaceGridLayout());
+
+               // TODO: bi-directional
+               rightArea = new Composite(parent, SWT.NONE);
+               rightArea.setLayoutData(new GridData(SWT.END, SWT.TOP, false, false));
+               rightArea.setLayout(CmsUtils.noSpaceGridLayout());
+
+               footerArea = new Composite(parent, SWT.NONE);
+               // footerArea.setLayout(new FillLayout());
+               GridData footerData = new GridData(SWT.FILL, SWT.FILL, false, false, 3, 1);
+               footerData.heightHint = footerHeight;
+               footerArea.setLayoutData(footerData);
+
+               uiInitialized = true;
+               refresh();
+       }
+
+       @Override
+       protected void refresh() {
+               if (!uiInitialized)
+                       return;
+               if (getState() == null)
+                       setState("");
+               refreshSides();
+               refreshBody();
+               if (log.isTraceEnabled())
+                       log.trace("UI refreshed " + getNode());
+       }
+
+       protected void createAdminArea(Composite parent) {
+       }
+
+       @Deprecated
+       protected void refreshHeader() {
+               if (header == null)
+                       return;
+
+               for (Control child : headerArea.getChildren())
+                       child.dispose();
+               try {
+                       header.createUi(headerArea, getNode());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot refresh header", e);
+               }
+               headerArea.layout(true, true);
+       }
+
+       protected void refreshSides() {
+               refresh(headerArea, header, CmsStyles.CMS_HEADER);
+               refresh(leftArea, lead, CmsStyles.CMS_LEAD);
+               refresh(rightArea, end, CmsStyles.CMS_END);
+               refresh(footerArea, footer, CmsStyles.CMS_FOOTER);
+       }
+
+       private void refresh(Composite area, CmsUiProvider uiProvider, String style) {
+               if (uiProvider == null)
+                       return;
+
+               for (Control child : area.getChildren())
+                       child.dispose();
+               CmsUtils.style(area, style);
+               try {
+                       uiProvider.createUi(area, getNode());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot refresh header", e);
+               }
+               area.layout(true, true);
+       }
+
+       protected void refreshBody() {
+               // Exception
+               Throwable exception = getException();
+               if (exception != null) {
+                       SystemNotifications systemNotifications = new SystemNotifications(bodyArea);
+                       systemNotifications.notifyException(exception);
+                       resetException();
+                       return;
+                       // TODO report
+               }
+
+               // clear
+               for (Control child : bodyArea.getChildren())
+                       child.dispose();
+               bodyArea.setLayout(CmsUtils.noSpaceGridLayout());
+
+               try {
+                       Node node = getNode();
+//                     if (node == null)
+//                             log.error("Context cannot be null");
+//                     else
+                       uiProvider.createUi(bodyArea, node);
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot refresh body", e);
+               }
+
+               bodyArea.layout(true, true);
+       }
+
+       @Override
+       public UxContext getUxContext() {
+               return uxContext;
+       }
+
+       @Override
+       public CmsImageManager getImageManager() {
+               return imageManager;
+       }
+
+       public void setHeader(CmsUiProvider header) {
+               this.header = header;
+       }
+
+       public void setHeaderHeight(Integer headerHeight) {
+               this.headerHeight = headerHeight;
+       }
+
+       public void setImageManager(CmsImageManager imageManager) {
+               this.imageManager = imageManager;
+       }
+
+       public CmsUiProvider getLead() {
+               return lead;
+       }
+
+       public void setLead(CmsUiProvider lead) {
+               this.lead = lead;
+       }
+
+       public CmsUiProvider getEnd() {
+               return end;
+       }
+
+       public void setEnd(CmsUiProvider end) {
+               this.end = end;
+       }
+
+       public CmsUiProvider getFooter() {
+               return footer;
+       }
+
+       public void setFooter(CmsUiProvider footer) {
+               this.footer = footer;
+       }
+
+       public CmsUiProvider getHeader() {
+               return header;
+       }
+
+       public void setFooterHeight(Integer footerHeight) {
+               this.footerHeight = footerHeight;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleImageManager.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleImageManager.java
new file mode 100644 (file)
index 0000000..77d61cb
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.cms.util;
+
+import org.argeo.cms.ui.internal.ImageManagerImpl;
+
+public class SimpleImageManager extends ImageManagerImpl {
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleStaticPage.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleStaticPage.java
new file mode 100644 (file)
index 0000000..8ac3e96
--- /dev/null
@@ -0,0 +1,32 @@
+package org.argeo.cms.util;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.CmsStyles;
+import org.argeo.cms.ui.CmsUiProvider;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+
+public class SimpleStaticPage implements CmsUiProvider {
+       private String text;
+
+       @Override
+       public Control createUi(Composite parent, Node context)
+                       throws RepositoryException {
+               Label textC = new Label(parent,  SWT.WRAP);
+               textC.setData(RWT.CUSTOM_VARIANT, CmsStyles.CMS_STATIC_TEXT);
+               textC.setData(RWT.MARKUP_ENABLED, Boolean.TRUE);
+               textC.setText(text);
+               
+               return textC;
+       }
+
+       public void setText(String text) {
+               this.text = text;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleUxContext.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SimpleUxContext.java
new file mode 100644 (file)
index 0000000..bde67e4
--- /dev/null
@@ -0,0 +1,50 @@
+package org.argeo.cms.util;
+
+import org.argeo.cms.ui.UxContext;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Display;
+
+public class SimpleUxContext implements UxContext {
+       private Point size;
+       private Point small = new Point(400, 400);
+
+       public SimpleUxContext() {
+               this(Display.getCurrent().getBounds());
+       }
+
+       public SimpleUxContext(Rectangle rect) {
+               this.size = new Point(rect.width, rect.height);
+       }
+
+       public SimpleUxContext(Point size) {
+               this.size = size;
+       }
+
+       @Override
+       public boolean isPortrait() {
+               return size.x >= size.y;
+       }
+
+       @Override
+       public boolean isLandscape() {
+               return size.x < size.y;
+       }
+
+       @Override
+       public boolean isSquare() {
+               return size.x == size.y;
+       }
+
+       @Override
+       public boolean isSmall() {
+               return size.x <= small.x || size.y <= small.y;
+       }
+
+       @Override
+       public boolean isMasterData() {
+               // TODO make it configurable
+               return true;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/StyleSheetResourceLoader.java b/org.argeo.cms.ui/src/org/argeo/cms/util/StyleSheetResourceLoader.java
new file mode 100644 (file)
index 0000000..face42b
--- /dev/null
@@ -0,0 +1,71 @@
+package org.argeo.cms.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.CmsException;
+import org.eclipse.rap.rwt.service.ResourceLoader;
+import org.osgi.framework.Bundle;
+
+/** {@link ResourceLoader} caching stylesheets. */
+public class StyleSheetResourceLoader implements ResourceLoader {
+       private Bundle themeBundle;
+       private Map<String, StyleSheet> stylesheets = new LinkedHashMap<String, StyleSheet>();
+
+       public StyleSheetResourceLoader(Bundle themeBundle) {
+               this.themeBundle = themeBundle;
+       }
+
+       @Override
+       public InputStream getResourceAsStream(String resourceName) throws IOException {
+               if (!stylesheets.containsKey(resourceName)) {
+                       // TODO deal with other bundles
+                       // Bundle bundle = bundleContext.getBundle();
+                       // String location =
+                       // bundle.getLocation().substring("initial@reference:".length());
+                       // if (location.startsWith("file:")) {
+                       // Path path = null;
+                       // try {
+                       // path = Paths.get(new URI(location));
+                       // } catch (URISyntaxException e) {
+                       // e.printStackTrace();
+                       // }
+                       // if (path != null) {
+                       // Path resourcePath = path.resolve(resourceName);
+                       // if (Files.exists(resourcePath))
+                       // return Files.newInputStream(resourcePath);
+                       // }
+                       // }
+
+                       URL res = themeBundle.getEntry(resourceName);
+                       if (res == null)
+                               throw new CmsException(
+                                               "Entry " + resourceName + " not found in bundle " + themeBundle.getSymbolicName());
+                       ByteArrayOutputStream out = new ByteArrayOutputStream();
+                       IOUtils.copy(res.openStream(), out);
+                       stylesheets.put(resourceName, new StyleSheet(out.toByteArray()));
+               }
+               return new ByteArrayInputStream(stylesheets.get(resourceName).getData());
+               // return res.openStream();
+       }
+
+       private class StyleSheet {
+               private byte[] data;
+
+               public StyleSheet(byte[] data) {
+                       super();
+                       this.data = data;
+               }
+
+               public byte[] getData() {
+                       return data;
+               }
+
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/SystemNotifications.java b/org.argeo.cms.ui/src/org/argeo/cms/util/SystemNotifications.java
new file mode 100644 (file)
index 0000000..5fa79a3
--- /dev/null
@@ -0,0 +1,128 @@
+package org.argeo.cms.util;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.CmsStyles;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/** Shell displaying system notifications such as exceptions */
+public class SystemNotifications extends Shell implements CmsStyles,
+               MouseListener {
+       private static final long serialVersionUID = -8129377525216022683L;
+
+       private Control source;
+
+       public SystemNotifications(Control source) {
+               super(source.getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+               setData(RWT.CUSTOM_VARIANT, CMS_USER_MENU);
+
+               this.source = source;
+
+               // TODO UI
+               // setLocation(source.toDisplay(source.getSize().x - getSize().x,
+               // source.getSize().y));
+               setLayout(new GridLayout());
+               addMouseListener(this);
+
+               addShellListener(new ShellAdapter() {
+                       private static final long serialVersionUID = 5178980294808435833L;
+
+                       @Override
+                       public void shellDeactivated(ShellEvent e) {
+                               close();
+                               dispose();
+                       }
+               });
+
+       }
+
+       public void notifyException(Throwable exception) {
+               Composite pane = this;
+
+               Label lbl = new Label(pane, SWT.NONE);
+               lbl.setText(exception.getLocalizedMessage()
+                               + (exception instanceof CmsException ? "" : "("
+                                               + exception.getClass().getName() + ")") + "\n");
+               lbl.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               lbl.addMouseListener(this);
+               if (exception.getCause() != null)
+                       appendCause(pane, exception.getCause());
+
+               StringBuilder mailToUrl = new StringBuilder("mailto:?");
+               try {
+                       mailToUrl.append("subject=").append(
+                                       URLEncoder.encode(
+                                                       "Exception "
+                                                                       + new SimpleDateFormat("yyyy-MM-dd hh:mm")
+                                                                                       .format(new Date()), "UTF-8")
+                                                       .replace("+", "%20"));
+
+                       StringWriter sw = new StringWriter();
+                       exception.printStackTrace(new PrintWriter(sw));
+                       IOUtils.closeQuietly(sw);
+
+                       // see
+                       // http://stackoverflow.com/questions/4737841/urlencoder-not-able-to-translate-space-character
+                       String encoded = URLEncoder.encode(sw.toString(), "UTF-8").replace(
+                                       "+", "%20");
+                       mailToUrl.append("&amp;body=").append(encoded);
+               } catch (UnsupportedEncodingException e) {
+                       mailToUrl.append("&amp;body=").append("Could not encode: ")
+                                       .append(e.getMessage());
+               }
+               Label mailTo = new Label(pane, SWT.NONE);
+               CmsUtils.markup(mailTo);
+               mailTo.setText("<a href=\"" + mailToUrl + "\">Send details</a>");
+               mailTo.setLayoutData(new GridData(SWT.END, SWT.FILL, true, false));
+
+               pack();
+               layout();
+
+               setLocation(source.toDisplay(source.getSize().x - getSize().x,
+                               source.getSize().y - getSize().y));
+               open();
+       }
+
+       private void appendCause(Composite parent, Throwable e) {
+               Label lbl = new Label(parent, SWT.NONE);
+               lbl.setText(" caused by: " + e.getLocalizedMessage() + " ("
+                               + e.getClass().getName() + ")" + "\n");
+               lbl.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               lbl.addMouseListener(this);
+               if (e.getCause() != null)
+                       appendCause(parent, e.getCause());
+       }
+
+       @Override
+       public void mouseDoubleClick(MouseEvent e) {
+       }
+
+       @Override
+       public void mouseDown(MouseEvent e) {
+               close();
+               dispose();
+       }
+
+       @Override
+       public void mouseUp(MouseEvent e) {
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/ThemeUtils.java b/org.argeo.cms.ui/src/org/argeo/cms/util/ThemeUtils.java
new file mode 100644 (file)
index 0000000..fdc1cb7
--- /dev/null
@@ -0,0 +1,50 @@
+package org.argeo.cms.util;
+
+import java.net.URL;
+import java.util.Enumeration;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.eclipse.rap.rwt.application.Application;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+public class ThemeUtils {
+       final static Log log = LogFactory.getLog(ThemeUtils.class);
+
+       public static Bundle findThemeBundle(BundleContext bundleContext, String themeId) {
+               if (themeId == null)
+                       return null;
+               // TODO optimize
+               // TODO deal with multiple versions
+               Bundle themeBundle = null;
+               if (themeId != null) {
+                       for (Bundle bundle : bundleContext.getBundles())
+                               if (themeId.equals(bundle.getSymbolicName())) {
+                                       themeBundle = bundle;
+                                       break;
+                               }
+               }
+               return themeBundle;
+       }
+
+       public static void addThemeResources(Application application, Bundle themeBundle, BundleResourceLoader themeBRL,
+                       String pattern) {
+               Enumeration<URL> themeResources = themeBundle.findEntries("/", pattern, true);
+               if (themeResources == null)
+                       return;
+               while (themeResources.hasMoreElements()) {
+                       String resource = themeResources.nextElement().getPath();
+                       // remove first '/' so that RWT registers it
+                       resource = resource.substring(1);
+                       if (!resource.endsWith("/")) {
+                               application.addResource(resource, themeBRL);
+                               if (log.isTraceEnabled())
+                                       log.trace("Registered " + resource + " from theme " + themeBundle);
+                       }
+
+               }
+
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/UserAdminUtils.java b/org.argeo.cms.ui/src/org/argeo/cms/util/UserAdminUtils.java
new file mode 100644 (file)
index 0000000..65f99c5
--- /dev/null
@@ -0,0 +1,172 @@
+package org.argeo.cms.util;
+
+import java.util.List;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeConstants;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+/** Centralise common patterns to manage users with a {@link UserAdmin} */
+public class UserAdminUtils {
+
+       // CURRENTUSER HELPERS
+       /** Checks if current user is the same as the passed one */
+       public static boolean isCurrentUser(User user) {
+               String userUsername = getProperty(user, LdapAttrs.DN);
+               LdapName userLdapName = getLdapName(userUsername);
+               LdapName selfUserName = getCurrentUserLdapName();
+               return userLdapName.equals(selfUserName);
+       }
+
+       /** Retrieves the current logged-in {@link User} */
+       public static User getCurrentUser(UserAdmin userAdmin) {
+               return (User) userAdmin.getRole(CurrentUser.getUsername());
+       }
+
+       /** Retrieves the current logged-in user {@link LdapName} */
+       public final static LdapName getCurrentUserLdapName() {
+               String name = CurrentUser.getUsername();
+               return getLdapName(name);
+       }
+
+       /** Retrieves the current logged-in user mail */
+       public static String getCurrentUserMail(UserAdmin userAdmin) {
+               String username = CurrentUser.getUsername();
+               return getUserMail(userAdmin, username);
+       }
+
+       /** Retrieves the current logged-in user common name */
+       public final static String getCommonName(User user) {
+               return getProperty(user, LdapAttrs.cn.name());
+       }
+
+       // OTHER USERS HELPERS
+       /**
+        * Retrieves the local id of a user or group, that is respectively the uid or cn
+        * of the passed dn with no {@link UserAdmin}
+        */
+       public static String getUserLocalId(String dn) {
+               LdapName ldapName = getLdapName(dn);
+               Rdn last = ldapName.getRdn(ldapName.size() - 1);
+               if (last.getType().toLowerCase().equals(LdapAttrs.uid.name())
+                               || last.getType().toLowerCase().equals(LdapAttrs.cn.name()))
+                       return (String) last.getValue();
+               else
+                       throw new CmsException("Cannot retrieve user local id, non valid dn: " + dn);
+       }
+
+       /**
+        * Returns the local username if no user with this dn is found or if the found
+        * user has no defined display name
+        */
+       public static String getUserDisplayName(UserAdmin userAdmin, String dn) {
+               Role user = userAdmin.getRole(dn);
+               String dName;
+               if (user == null)
+                       dName = getUserLocalId(dn);
+               else {
+                       dName = getProperty(user, LdapAttrs.displayName.name());
+                       if (EclipseUiUtils.isEmpty(dName))
+                               dName = getProperty(user, LdapAttrs.cn.name());
+                       if (EclipseUiUtils.isEmpty(dName))
+                               dName = getUserLocalId(dn);
+               }
+               return dName;
+       }
+
+       /**
+        * Returns null if no user with this dn is found or if the found user has no
+        * defined mail
+        */
+       public static String getUserMail(UserAdmin userAdmin, String dn) {
+               Role user = userAdmin.getRole(dn);
+               if (user == null)
+                       return null;
+               else
+                       return getProperty(user, LdapAttrs.mail.name());
+       }
+
+       // LDAP NAMES HELPERS
+       /**
+        * Easily retrieves one of the {@link Role}'s property or an empty String if the
+        * requested property is not defined
+        */
+       public final static String getProperty(Role role, String key) {
+               Object obj = role.getProperties().get(key);
+               if (obj != null)
+                       return (String) obj;
+               else
+                       return "";
+       }
+
+       public final static String getProperty(Role role, Enum<?> key) {
+               Object obj = role.getProperties().get(key.name());
+               if (obj != null)
+                       return (String) obj;
+               else
+                       return "";
+       }
+
+       @SuppressWarnings("unchecked")
+       public final static void setProperty(Role role, String key, String value) {
+               role.getProperties().put(key, value);
+       }
+
+       public final static void setProperty(Role role, Enum<?> key, String value) {
+               setProperty(role, key.name(), value);
+       }
+
+       /**
+        * Simply retrieves a LDAP name from a {@link LdapAttrs.DN} with no exception
+        */
+       private static LdapName getLdapName(String dn) {
+               try {
+                       return new LdapName(dn);
+               } catch (InvalidNameException e) {
+                       throw new CmsException("Cannot parse LDAP name " + dn, e);
+               }
+       }
+
+       /** Simply retrieves a display name of the relevant domain */
+       public final static String getDomainName(User user) {
+               String dn = user.getName();
+               if (dn.endsWith(NodeConstants.ROLES_BASEDN))
+                       return "System roles";
+               if (dn.endsWith(NodeConstants.TOKENS_BASEDN))
+                       return "Tokens";
+               try {
+                       // FIXME deal with non-DC
+                       LdapName name = new LdapName(dn);
+                       List<Rdn> rdns = name.getRdns();
+                       String dname = null;
+                       int i = 0;
+                       loop: while (i < rdns.size()) {
+                               Rdn currrRdn = rdns.get(i);
+                               if (!LdapAttrs.dc.name().equals(currrRdn.getType()))
+                                       break loop;
+                               else {
+                                       String currVal = (String) currrRdn.getValue();
+                                       dname = dname == null ? currVal : currVal + "." + dname;
+                               }
+                               i++;
+                       }
+                       return dname;
+               } catch (InvalidNameException e) {
+                       throw new CmsException("Unable to get domain name for " + dn, e);
+               }
+       }
+
+       // VARIOUS HELPERS
+       public final static String buildDefaultCn(String firstName, String lastName) {
+               return (firstName.trim() + " " + lastName.trim() + " ").trim();
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/UserMenu.java b/org.argeo.cms.ui/src/org/argeo/cms/util/UserMenu.java
new file mode 100644 (file)
index 0000000..58b470d
--- /dev/null
@@ -0,0 +1,55 @@
+package org.argeo.cms.util;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.widgets.auth.CmsLoginShell;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+/** The site-related user menu */
+public class UserMenu extends CmsLoginShell {
+       private final Control source;
+       private final Node context;
+
+       public UserMenu(Control source, Node context) {
+               super(CmsUtils.getCmsView());
+               this.context = context;
+               createUi();
+               if (source == null)
+                       throw new CmsException("Source control cannot be null.");
+               this.source = source;
+               open();
+       }
+
+       @Override
+       protected Shell createShell() {
+               return new Shell(Display.getCurrent(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+       }
+
+       @Override
+       public void open() {
+               Shell shell = getShell();
+               shell.pack();
+               shell.layout();
+               shell.setLocation(source.toDisplay(source.getSize().x - shell.getSize().x, source.getSize().y));
+               shell.addShellListener(new ShellAdapter() {
+                       private static final long serialVersionUID = 5178980294808435833L;
+
+                       @Override
+                       public void shellDeactivated(ShellEvent e) {
+                               closeShell();
+                       }
+               });
+               super.open();
+       }
+
+       protected Node getContext() {
+               return context;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/UserMenuLink.java b/org.argeo.cms.ui/src/org/argeo/cms/util/UserMenuLink.java
new file mode 100644 (file)
index 0000000..839567f
--- /dev/null
@@ -0,0 +1,84 @@
+package org.argeo.cms.util;
+
+import javax.jcr.Node;
+
+import org.argeo.cms.CmsMsg;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.CmsStyles;
+import org.argeo.cms.widgets.auth.CmsLoginShell;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+
+/** Open the user menu when clicked */
+public class UserMenuLink extends MenuLink {
+
+       public UserMenuLink() {
+               setCustom(CmsStyles.CMS_USER_MENU_LINK);
+       }
+
+       @Override
+       public Control createUi(Composite parent, Node context) {
+               if (CurrentUser.isAnonymous())
+                       setLabel(CmsMsg.login.lead());
+               else {
+                       setLabel(CurrentUser.getDisplayName());
+               }
+               Label link = (Label) ((Composite) super.createUi(parent, context)).getChildren()[0];
+               link.addMouseListener(new UserMenuLinkController(context));
+               return link.getParent();
+       }
+
+       protected CmsLoginShell createUserMenu(Control source, Node context) {
+               return new UserMenu(source.getParent(), context);
+       }
+
+       private class UserMenuLinkController implements MouseListener, DisposeListener {
+               private static final long serialVersionUID = 3634864186295639792L;
+
+               private CmsLoginShell userMenu = null;
+               private long lastDisposeTS = 0l;
+
+               private final Node context;
+
+               public UserMenuLinkController(Node context) {
+                       this.context = context;
+               }
+
+               //
+               // MOUSE LISTENER
+               //
+               @Override
+               public void mouseDown(MouseEvent e) {
+                       if (e.button == 1) {
+                               Control source = (Control) e.getSource();
+                               if (userMenu == null) {
+                                       long durationSinceLastDispose = System.currentTimeMillis() - lastDisposeTS;
+                                       // avoid to reopen the menu, if one has clicked gain
+                                       if (durationSinceLastDispose > 200) {
+                                               userMenu = createUserMenu(source, context);
+                                               userMenu.getShell().addDisposeListener(this);
+                                       }
+                               }
+                       }
+               }
+
+               @Override
+               public void mouseDoubleClick(MouseEvent e) {
+               }
+
+               @Override
+               public void mouseUp(MouseEvent e) {
+               }
+
+               @Override
+               public void widgetDisposed(DisposeEvent event) {
+                       userMenu = null;
+                       lastDisposeTS = System.currentTimeMillis();
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/util/VerticalMenu.java b/org.argeo.cms.ui/src/org/argeo/cms/util/VerticalMenu.java
new file mode 100644 (file)
index 0000000..d0ea610
--- /dev/null
@@ -0,0 +1,43 @@
+package org.argeo.cms.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.ui.CmsUiProvider;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+public class VerticalMenu implements CmsUiProvider {
+       private List<CmsUiProvider> items = new ArrayList<CmsUiProvider>();
+
+       @Override
+       public Control createUi(Composite parent, Node context) throws RepositoryException {
+               Composite part = new Composite(parent, SWT.NONE);
+               part.setLayoutData(new GridData(SWT.LEAD, SWT.TOP, false, false));
+//             part.setData(RWT.CUSTOM_VARIANT, custom);
+               part.setLayout(CmsUtils.noSpaceGridLayout());
+               for (CmsUiProvider uiProvider : items) {
+                       Control subPart = uiProvider.createUi(part, context);
+                       subPart.setLayoutData(new GridData(SWT.LEAD, SWT.TOP, false, false));
+               }
+               return part;
+       }
+
+       public void add(CmsUiProvider uiProvider) {
+               items.add(uiProvider);
+       }
+
+       public List<CmsUiProvider> getItems() {
+               return items;
+       }
+
+       public void setItems(List<CmsUiProvider> items) {
+               this.items = items;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/viewers/AbstractPageViewer.java b/org.argeo.cms.ui/src/org/argeo/cms/viewers/AbstractPageViewer.java
new file mode 100644 (file)
index 0000000..492203b
--- /dev/null
@@ -0,0 +1,324 @@
+package org.argeo.cms.viewers;
+
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Observable;
+import java.util.Observer;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.widgets.ScrolledPage;
+import org.eclipse.jface.viewers.ContentViewer;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Widget;
+import org.xml.sax.SAXParseException;
+
+/** Base class for viewers related to a page */
+public abstract class AbstractPageViewer extends ContentViewer implements Observer {
+       private static final long serialVersionUID = 5438688173410341485L;
+
+       private final static Log log = LogFactory.getLog(AbstractPageViewer.class);
+
+       private final boolean readOnly;
+       /** The basis for the layouts, typically a ScrolledPage. */
+       private final Composite page;
+       private final CmsEditable cmsEditable;
+
+       private MouseListener mouseListener;
+       private FocusListener focusListener;
+
+       private EditablePart edited;
+       private ISelection selection = StructuredSelection.EMPTY;
+
+       private AccessControlContext accessControlContext;
+
+       protected AbstractPageViewer(Section parent, int style, CmsEditable cmsEditable) {
+               // read only at UI level
+               readOnly = SWT.READ_ONLY == (style & SWT.READ_ONLY);
+
+               this.cmsEditable = cmsEditable == null ? CmsEditable.NON_EDITABLE : cmsEditable;
+               if (this.cmsEditable instanceof Observable)
+                       ((Observable) this.cmsEditable).addObserver(this);
+
+               if (cmsEditable.canEdit()) {
+                       mouseListener = createMouseListener();
+                       focusListener = createFocusListener();
+               }
+               page = findPage(parent);
+               accessControlContext = AccessController.getContext();
+       }
+
+       /**
+        * Can be called to simplify the called to isModelInitialized() and initModel()
+        */
+       protected void initModelIfNeeded(Node node) {
+               try {
+                       if (!isModelInitialized(node))
+                               if (getCmsEditable().canEdit()) {
+                                       initModel(node);
+                                       node.getSession().save();
+                               }
+               } catch (Exception e) {
+                       throw new CmsException("Cannot initialize model", e);
+               }
+       }
+
+       /** Called if user can edit and model is not initialized */
+       protected Boolean isModelInitialized(Node node) throws RepositoryException {
+               return true;
+       }
+
+       /** Called if user can edit and model is not initialized */
+       protected void initModel(Node node) throws RepositoryException {
+       }
+
+       /** Create (retrieve) the MouseListener to use. */
+       protected MouseListener createMouseListener() {
+               return new MouseAdapter() {
+                       private static final long serialVersionUID = 1L;
+               };
+       }
+
+       /** Create (retrieve) the FocusListener to use. */
+       protected FocusListener createFocusListener() {
+               return new FocusListener() {
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       public void focusLost(FocusEvent event) {
+                       }
+
+                       @Override
+                       public void focusGained(FocusEvent event) {
+                       }
+               };
+       }
+
+       protected Composite findPage(Composite composite) {
+               if (composite instanceof ScrolledPage) {
+                       return (ScrolledPage) composite;
+               } else {
+                       if (composite.getParent() == null)
+                               return composite;
+                       return findPage(composite.getParent());
+               }
+       }
+
+       @Override
+       public void update(Observable o, Object arg) {
+               if (o == cmsEditable)
+                       editingStateChanged(cmsEditable);
+       }
+
+       /** To be overridden in order to provide the actual refresh */
+       protected void refresh(Control control) throws RepositoryException {
+       }
+
+       /** To be overridden.Save the edited part. */
+       protected void save(EditablePart part) throws RepositoryException {
+       }
+
+       /** Prepare the edited part */
+       protected void prepare(EditablePart part, Object caretPosition) {
+       }
+
+       /** Notified when the editing state changed. Does nothing, to be overridden */
+       protected void editingStateChanged(CmsEditable cmsEditable) {
+       }
+
+       @Override
+       public void refresh() {
+               // TODO check actual context in order to notice a discrepancy
+               Subject viewerSubject = getViewerSubject();
+               Subject.doAs(viewerSubject, (PrivilegedAction<Void>) () -> {
+                       try {
+                               if (cmsEditable.canEdit() && !readOnly)
+                                       mouseListener = createMouseListener();
+                               else
+                                       mouseListener = null;
+                               refresh(getControl());
+                               layout(getControl());
+                       } catch (RepositoryException e) {
+                               throw new CmsException("Cannot refresh", e);
+                       }
+                       return null;
+               });
+       }
+
+       @Override
+       public void setSelection(ISelection selection, boolean reveal) {
+               this.selection = selection;
+       }
+
+       protected void updateContent(EditablePart part) throws RepositoryException {
+       }
+
+       // LOW LEVEL EDITION
+       protected void edit(EditablePart part, Object caretPosition) {
+               try {
+                       if (edited == part)
+                               return;
+
+                       if (edited != null && edited != part) {
+                               EditablePart previouslyEdited = edited;
+                               try {
+                                       stopEditing(true);
+                               } catch (Exception e) {
+                                       notifyEditionException(e);
+                                       edit(previouslyEdited, caretPosition);
+                                       return;
+                               }
+                       }
+
+                       part.startEditing();
+                       updateContent(part);
+                       prepare(part, caretPosition);
+                       edited = part;
+                       layout(part.getControl());
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot edit " + part, e);
+               }
+       }
+
+       private void stopEditing(Boolean save) throws RepositoryException {
+               if (edited instanceof Widget && ((Widget) edited).isDisposed()) {
+                       edited = null;
+                       return;
+               }
+
+               assert edited != null;
+               if (edited == null) {
+                       if (log.isTraceEnabled())
+                               log.warn("Told to stop editing while not editing anything");
+                       return;
+               }
+
+               if (save)
+                       save(edited);
+
+               edited.stopEditing();
+               updateContent(edited);
+               layout(((EditablePart) edited).getControl());
+               edited = null;
+       }
+
+       // METHODS AVAILABLE TO EXTENDING CLASSES
+       protected void saveEdit() {
+               try {
+                       if (edited != null)
+                               stopEditing(true);
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot stop editing", e);
+               }
+       }
+
+       protected void cancelEdit() {
+               try {
+                       if (edited != null)
+                               stopEditing(false);
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot cancel editing", e);
+               }
+       }
+
+       /** Layout this controls from the related base page. */
+       public void layout(Control... controls) {
+               page.layout(controls);
+       }
+
+       /**
+        * Find the first {@link EditablePart} in the parents hierarchy of this control
+        */
+       protected EditablePart findDataParent(Control parent) {
+               if (parent instanceof EditablePart) {
+                       return (EditablePart) parent;
+               }
+               if (parent.getParent() != null)
+                       return findDataParent(parent.getParent());
+               else
+                       throw new CmsException("No data parent found");
+       }
+
+       // UTILITIES
+       /** Check whether the edited part is in a proper state */
+       protected void checkEdited() {
+               if (edited == null || (edited instanceof Widget) && ((Widget) edited).isDisposed())
+                       throw new CmsException("Edited should not be null or disposed at this stage");
+       }
+
+       /** Persist all changes. */
+       protected void persistChanges(Session session) throws RepositoryException {
+               session.save();
+               session.refresh(false);
+               // TODO notify that changes have been persisted
+       }
+
+       /** Convenience method using a Node in order to save the underlying session. */
+       protected void persistChanges(Node anyNode) throws RepositoryException {
+               persistChanges(anyNode.getSession());
+       }
+
+       /** Notify edition exception */
+       protected void notifyEditionException(Throwable e) {
+               Throwable eToLog = e;
+               if (e instanceof IllegalArgumentException)
+                       if (e.getCause() instanceof SAXParseException)
+                               eToLog = e.getCause();
+               log.error(eToLog.getMessage());
+               if (log.isTraceEnabled())
+                       log.trace("Full stack of " + eToLog.getMessage(), e);
+               // TODO Light error notification popup
+       }
+
+       protected Subject getViewerSubject() {
+               Subject res = null;
+               if (accessControlContext != null) {
+                       res = Subject.getSubject(accessControlContext);
+               }
+               if (res == null)
+                       throw new CmsException("No subject associated with this viewer");
+               return res;
+       }
+
+       // GETTERS / SETTERS
+       public boolean isReadOnly() {
+               return readOnly;
+       }
+
+       protected EditablePart getEdited() {
+               return edited;
+       }
+
+       public MouseListener getMouseListener() {
+               return mouseListener;
+       }
+
+       public FocusListener getFocusListener() {
+               return focusListener;
+       }
+
+       public CmsEditable getCmsEditable() {
+               return cmsEditable;
+       }
+
+       @Override
+       public ISelection getSelection() {
+               return selection;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/viewers/EditablePart.java b/org.argeo.cms.ui/src/org/argeo/cms/viewers/EditablePart.java
new file mode 100644 (file)
index 0000000..99f8acf
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.cms.viewers;
+
+import org.eclipse.swt.widgets.Control;
+
+public interface EditablePart {
+       public void startEditing();
+
+       public void stopEditing();
+
+       public Control getControl();
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/viewers/ItemPart.java b/org.argeo.cms.ui/src/org/argeo/cms/viewers/ItemPart.java
new file mode 100644 (file)
index 0000000..52e5a88
--- /dev/null
@@ -0,0 +1,9 @@
+package org.argeo.cms.viewers;
+
+import javax.jcr.Item;
+import javax.jcr.RepositoryException;
+
+/** An editable part related to a JCR Item */
+public interface ItemPart<T extends Item> {
+       public Item getItem() throws RepositoryException;
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/viewers/JcrVersionCmsEditable.java b/org.argeo.cms.ui/src/org/argeo/cms/viewers/JcrVersionCmsEditable.java
new file mode 100644 (file)
index 0000000..bc31663
--- /dev/null
@@ -0,0 +1,101 @@
+package org.argeo.cms.viewers;
+
+import java.util.Observable;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.version.VersionManager;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.ui.CmsEditable;
+import org.argeo.cms.ui.CmsEditionEvent;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+
+/** Provides the CmsEditable semantic based on JCR versioning. */
+public class JcrVersionCmsEditable extends Observable implements CmsEditable {
+       private final String nodePath;// cache
+       private final VersionManager versionManager;
+       private final Boolean canEdit;
+
+       public JcrVersionCmsEditable(Node node) throws RepositoryException {
+               this.nodePath = node.getPath();
+               if (node.getSession().hasPermission(node.getPath(),
+                               Session.ACTION_SET_PROPERTY)) {
+                       // was Session.ACTION_ADD_NODE
+                       canEdit = true;
+                       if (!node.isNodeType(NodeType.MIX_VERSIONABLE)) {
+                               node.addMixin(NodeType.MIX_VERSIONABLE);
+                               node.getSession().save();
+                       }
+                       versionManager = node.getSession().getWorkspace()
+                                       .getVersionManager();
+               } else {
+                       canEdit = false;
+                       versionManager = null;
+               }
+
+               // bind keys
+               if (canEdit) {
+                       Display display = Display.getCurrent();
+                       display.setData(RWT.ACTIVE_KEYS, new String[] { "CTRL+RETURN",
+                                       "CTRL+E" });
+                       display.addFilter(SWT.KeyDown, new Listener() {
+                               private static final long serialVersionUID = -4378653870463187318L;
+
+                               public void handleEvent(Event e) {
+                                       boolean ctrlPressed = (e.stateMask & SWT.CTRL) != 0;
+                                       if (ctrlPressed && e.keyCode == '\r')
+                                               stopEditing();
+                                       else if (ctrlPressed && e.keyCode == 'E')
+                                               stopEditing();
+                               }
+                       });
+               }
+       }
+
+       @Override
+       public Boolean canEdit() {
+               return canEdit;
+       }
+
+       public Boolean isEditing() {
+               try {
+                       if (!canEdit())
+                               return false;
+                       return versionManager.isCheckedOut(nodePath);
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot check whether " + nodePath
+                                       + " is editing", e);
+               }
+       }
+
+       @Override
+       public void startEditing() {
+               try {
+                       versionManager.checkout(nodePath);
+                       setChanged();
+               } catch (RepositoryException e1) {
+                       throw new CmsException("Cannot publish " + nodePath);
+               }
+               notifyObservers(new CmsEditionEvent(nodePath,
+                               CmsEditionEvent.START_EDITING));
+       }
+
+       @Override
+       public void stopEditing() {
+               try {
+                       versionManager.checkin(nodePath);
+                       setChanged();
+               } catch (RepositoryException e1) {
+                       throw new CmsException("Cannot publish " + nodePath, e1);
+               }
+               notifyObservers(new CmsEditionEvent(nodePath,
+                               CmsEditionEvent.STOP_EDITING));
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/viewers/NodePart.java b/org.argeo.cms.ui/src/org/argeo/cms/viewers/NodePart.java
new file mode 100644 (file)
index 0000000..db9a60a
--- /dev/null
@@ -0,0 +1,8 @@
+package org.argeo.cms.viewers;
+
+import javax.jcr.Node;
+
+/** An editable part related to a node */
+public interface NodePart extends ItemPart<Node> {
+       public Node getNode();
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/viewers/PropertyPart.java b/org.argeo.cms.ui/src/org/argeo/cms/viewers/PropertyPart.java
new file mode 100644 (file)
index 0000000..50fdd06
--- /dev/null
@@ -0,0 +1,8 @@
+package org.argeo.cms.viewers;
+
+import javax.jcr.Property;
+
+/** An editable part related to a JCR Property */
+public interface PropertyPart extends ItemPart<Property> {
+       public Property getProperty();
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/viewers/Section.java b/org.argeo.cms.ui/src/org/argeo/cms/viewers/Section.java
new file mode 100644 (file)
index 0000000..af7fd87
--- /dev/null
@@ -0,0 +1,155 @@
+package org.argeo.cms.viewers;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.cms.widgets.JcrComposite;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+public class Section extends JcrComposite implements CmsNames {
+       private static final long serialVersionUID = -5933796173755739207L;
+
+       private final Section parentSection;
+       private Composite sectionHeader;
+       private final Integer relativeDepth;
+
+       public Section(Composite parent, int style, Node node)
+                       throws RepositoryException {
+               this(parent, findSection(parent), style, node);
+       }
+
+       public Section(Section section, int style, Node node)
+                       throws RepositoryException {
+               this(section, section, style, node);
+       }
+
+       protected Section(Composite parent, Section parentSection, int style,
+                       Node node) throws RepositoryException {
+               super(parent, style, node);
+               this.parentSection = parentSection;
+               if (parentSection != null) {
+                       relativeDepth = getNode().getDepth()
+                                       - parentSection.getNode().getDepth();
+               } else {
+                       relativeDepth = 0;
+               }
+               setLayout(CmsUtils.noSpaceGridLayout());
+       }
+
+       public Map<String, Section> getSubSections() throws RepositoryException {
+               LinkedHashMap<String, Section> result = new LinkedHashMap<String, Section>();
+               for (Control child : getChildren()) {
+                       if (child instanceof Composite) {
+                               collectDirectSubSections((Composite) child, result);
+                       }
+               }
+               return Collections.unmodifiableMap(result);
+       }
+
+       private void collectDirectSubSections(Composite composite,
+                       LinkedHashMap<String, Section> subSections)
+                       throws RepositoryException {
+               if (composite == sectionHeader || composite instanceof EditablePart)
+                       return;
+               if (composite instanceof Section) {
+                       Section section = (Section) composite;
+                       subSections.put(section.getNodeId(), section);
+                       return;
+               }
+
+               for (Control child : composite.getChildren())
+                       if (child instanceof Composite)
+                               collectDirectSubSections((Composite) child, subSections);
+       }
+
+       public void createHeader() {
+               if (sectionHeader != null)
+                       throw new CmsException("Section header was already created");
+
+               sectionHeader = new Composite(this, SWT.NONE);
+               sectionHeader.setLayoutData(CmsUtils.fillWidth());
+               sectionHeader.setLayout(CmsUtils.noSpaceGridLayout());
+               // sectionHeader.moveAbove(null);
+               // layout();
+       }
+
+       public Composite getHeader() {
+               if (sectionHeader != null && sectionHeader.isDisposed())
+                       sectionHeader = null;
+               return sectionHeader;
+       }
+
+       // SECTION PARTS
+       public SectionPart getSectionPart(String partId) {
+               for (Control child : getChildren()) {
+                       if (child instanceof SectionPart) {
+                               SectionPart paragraph = (SectionPart) child;
+                               if (paragraph.getPartId().equals(partId))
+                                       return paragraph;
+                       }
+               }
+               return null;
+       }
+
+       public SectionPart nextSectionPart(SectionPart sectionPart) {
+               Control[] children = getChildren();
+               for (int i = 0; i < children.length; i++) {
+                       if (sectionPart == children[i])
+                               if (i + 1 < children.length) {
+                                       Composite next = (Composite) children[i + 1];
+                                       return (SectionPart) next;
+                               } else {
+                                       // next section
+                               }
+               }
+               return null;
+       }
+
+       public SectionPart previousSectionPart(SectionPart sectionPart) {
+               Control[] children = getChildren();
+               for (int i = 0; i < children.length; i++) {
+                       if (sectionPart == children[i])
+                               if (i != 0) {
+                                       Composite previous = (Composite) children[i - 1];
+                                       return (SectionPart) previous;
+                               } else {
+                                       // previous section
+                               }
+               }
+               return null;
+       }
+
+       @Override
+       public String toString() {
+               if (parentSection == null)
+                       return "Main section " + getNode();
+               return "Section " + getNode();
+       }
+
+       public Section getParentSection() {
+               return parentSection;
+       }
+
+       public Integer getRelativeDepth() {
+               return relativeDepth;
+       }
+
+       /** Recursively finds the related section in the parents (can be itself) */
+       public static Section findSection(Control control) {
+               if (control == null)
+                       return null;
+               if (control instanceof Section)
+                       return (Section) control;
+               else
+                       return findSection(control.getParent());
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/viewers/SectionPart.java b/org.argeo.cms.ui/src/org/argeo/cms/viewers/SectionPart.java
new file mode 100644 (file)
index 0000000..6cd45c5
--- /dev/null
@@ -0,0 +1,9 @@
+package org.argeo.cms.viewers;
+
+
+/** An editable part dynamically related to a Section */
+public interface SectionPart extends EditablePart, NodePart {
+       public String getPartId();
+
+       public Section getSection();
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/EditableImage.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/EditableImage.java
new file mode 100644 (file)
index 0000000..2e70eb8
--- /dev/null
@@ -0,0 +1,112 @@
+package org.argeo.cms.widgets;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** A stylable and editable image. */
+public abstract class EditableImage extends StyledControl {
+       private static final long serialVersionUID = -5689145523114022890L;
+       private final static Log log = LogFactory.getLog(EditableImage.class);
+
+       private Point preferredImageSize;
+       private Boolean loaded = false;
+
+       public EditableImage(Composite parent, int swtStyle) {
+               super(parent, swtStyle);
+       }
+
+       public EditableImage(Composite parent, int swtStyle,
+                       Point preferredImageSize) {
+               super(parent, swtStyle);
+               this.preferredImageSize = preferredImageSize;
+       }
+
+       public EditableImage(Composite parent, int style, Node node,
+                       boolean cacheImmediately, Point preferredImageSize)
+                       throws RepositoryException {
+               super(parent, style, node, cacheImmediately);
+               this.preferredImageSize = preferredImageSize;
+       }
+
+       @Override
+       protected void setContainerLayoutData(Composite composite) {
+               // composite.setLayoutData(fillWidth());
+       }
+
+       @Override
+       protected void setControlLayoutData(Control control) {
+               // control.setLayoutData(fillWidth());
+       }
+
+       /** To be overriden. */
+       protected String createImgTag() throws RepositoryException {
+               return CmsUtils.noImg(preferredImageSize != null ? preferredImageSize
+                               : getSize());
+       }
+
+       protected Label createLabel(Composite box, String style) {
+               Label lbl = new Label(box, getStyle());
+               // lbl.setLayoutData(CmsUtils.fillWidth());
+               CmsUtils.markup(lbl);
+               CmsUtils.style(lbl, style);
+               if (mouseListener != null)
+                       lbl.addMouseListener(mouseListener);
+               load(lbl);
+               return lbl;
+       }
+
+       /** To be overriden. */
+       protected synchronized Boolean load(Control control) {
+               String imgTag;
+               try {
+                       imgTag = createImgTag();
+               } catch (Exception e) {
+                       // throw new CmsException("Cannot retrieve image", e);
+                       log.error("Cannot retrieve image", e);
+                       imgTag = CmsUtils.noImg(preferredImageSize);
+                       loaded = false;
+               }
+
+               if (imgTag == null) {
+                       loaded = false;
+                       imgTag = CmsUtils.noImg(preferredImageSize);
+               } else
+                       loaded = true;
+               if (control != null) {
+                       ((Label) control).setText(imgTag);
+                       control.setSize(preferredImageSize != null ? preferredImageSize
+                                       : getSize());
+               } else {
+                       loaded = false;
+               }
+               getParent().layout();
+               return loaded;
+       }
+
+       public void setPreferredSize(Point size) {
+               this.preferredImageSize = size;
+               if (!loaded) {
+                       load((Label) getControl());
+               }
+       }
+
+       protected Text createText(Composite box, String style) {
+               Text text = new Text(box, getStyle());
+               CmsUtils.style(text, style);
+               return text;
+       }
+
+       public Point getPreferredImageSize() {
+               return preferredImageSize;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/EditableText.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/EditableText.java
new file mode 100644 (file)
index 0000000..e7c56ea
--- /dev/null
@@ -0,0 +1,76 @@
+package org.argeo.cms.widgets;
+
+import javax.jcr.Item;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** Editable text part displaying styled text. */
+public class EditableText extends StyledControl {
+       private static final long serialVersionUID = -6372283442330912755L;
+
+       public EditableText(Composite parent, int swtStyle) {
+               super(parent, swtStyle);
+       }
+
+       public EditableText(Composite parent, int style, Item item)
+                       throws RepositoryException {
+               this(parent, style, item, false);
+       }
+
+       public EditableText(Composite parent, int style, Item item,
+                       boolean cacheImmediately) throws RepositoryException {
+               super(parent, style, item, cacheImmediately);
+       }
+
+       @Override
+       protected Control createControl(Composite box, String style) {
+               if (isEditing())
+                       return createText(box, style);
+               else
+                       return createLabel(box, style);
+       }
+
+       protected Label createLabel(Composite box, String style) {
+               Label lbl = new Label(box, getStyle() | SWT.WRAP);
+               lbl.setLayoutData(CmsUtils.fillWidth());
+               CmsUtils.style(lbl, style);
+               CmsUtils.markup(lbl);
+               if (mouseListener != null)
+                       lbl.addMouseListener(mouseListener);
+               return lbl;
+       }
+
+       protected Text createText(Composite box, String style) {
+               final Text text = new Text(box, getStyle() | SWT.MULTI | SWT.WRAP);
+               GridData textLayoutData = CmsUtils.fillWidth();
+               // textLayoutData.heightHint = preferredHeight;
+               text.setLayoutData(textLayoutData);
+               CmsUtils.style(text, style);
+               text.setFocus();
+               return text;
+       }
+
+       public void setText(String text) {
+               Control child = getControl();
+               if (child instanceof Label)
+                       ((Label) child).setText(text);
+               else if (child instanceof Text)
+                       ((Text) child).setText(text);
+       }
+
+       public Text getAsText() {
+               return (Text) getControl();
+       }
+
+       public Label getAsLabel() {
+               return (Label) getControl();
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/JcrComposite.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/JcrComposite.java
new file mode 100644 (file)
index 0000000..358b453
--- /dev/null
@@ -0,0 +1,175 @@
+package org.argeo.cms.widgets;
+
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+
+/** A composite which can (optionally) manage a JCR Item. */
+public class JcrComposite extends Composite {
+       private static final long serialVersionUID = -1447009015451153367L;
+
+       private final Session session;
+
+       private String nodeId;
+       private String property = null;
+       private Node cache;
+
+       /** Regular composite constructor. No layout is set. */
+       public JcrComposite(Composite parent, int style) {
+               super(parent, style);
+               session = null;
+               nodeId = null;
+       }
+
+       public JcrComposite(Composite parent, int style, Item item)
+                       throws RepositoryException {
+               this(parent, style, item, false);
+       }
+
+       public JcrComposite(Composite parent, int style, Item item,
+                       boolean cacheImmediately) throws RepositoryException {
+               super(parent, style);
+               this.session = item.getSession();
+               if (!cacheImmediately && (SWT.READ_ONLY == (style & SWT.READ_ONLY))) {
+                       // (useless?) optimization: we only save a pointer to the session,
+                       // not even a reference to the item
+                       this.nodeId = null;
+               } else {
+                       Node node;
+                       Property property = null;
+                       if (item instanceof Node) {
+                               node = (Node) item;
+                       } else {// Property
+                               property = (Property) item;
+                               if (property.isMultiple())// TODO manage property index
+                                       throw new CmsException(
+                                                       "Multiple properties not supported yet.");
+                               this.property = property.getName();
+                               node = property.getParent();
+                       }
+                       this.nodeId = node.getIdentifier();
+                       if (cacheImmediately)
+                               this.cache = node;
+               }
+               setLayout(CmsUtils.noSpaceGridLayout());
+       }
+
+       public synchronized Node getNode() {
+               try {
+                       if (!itemIsNode())
+                               throw new CmsException("Item is not a Node");
+                       return getNodeInternal();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot get node " + nodeId, e);
+               }
+       }
+
+       private synchronized Node getNodeInternal() throws RepositoryException {
+               if (cache != null)
+                       return cache;
+               else if (session != null)
+                       if (nodeId != null)
+                               return session.getNodeByIdentifier(nodeId);
+                       else
+                               return null;
+               else
+                       return null;
+       }
+
+       public synchronized Property getProperty() {
+               try {
+                       if (itemIsNode())
+                               throw new CmsException("Item is not a Property");
+                       Node node = getNodeInternal();
+                       if (!node.hasProperty(property))
+                               throw new CmsException("Property " + property
+                                               + " is not set on " + node);
+                       return node.getProperty(property);
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot get property " + property
+                                       + " from node " + nodeId, e);
+               }
+       }
+
+       public synchronized Boolean itemIsNode() {
+               return property == null;
+       }
+
+       /** Set/update the cache or change the node */
+       public synchronized void setNode(Node node) throws RepositoryException {
+               if (!itemIsNode())
+                       throw new CmsException("Cannot set a Node on a Property");
+
+               if (node == null) {// clear cache
+                       this.cache = null;
+                       return;
+               }
+
+               if (session == null || session != node.getSession())// check session
+                       throw new CmsException("Uncompatible session");
+
+               if (nodeId == null || !nodeId.equals(node.getIdentifier())) {
+                       nodeId = node.getIdentifier();
+                       cache = node;
+                       itemUpdated();
+               } else {
+                       cache = node;// set/update cache
+               }
+       }
+
+       /** Set/update the cache or change the property */
+       public synchronized void setProperty(Property prop)
+                       throws RepositoryException {
+               if (itemIsNode())
+                       throw new CmsException("Cannot set a Property on a Node");
+
+               if (prop == null) {// clear cache
+                       this.cache = null;
+                       return;
+               }
+
+               if (session == null || session != prop.getSession())// check session
+                       throw new CmsException("Uncompatible session");
+
+               Node node = prop.getNode();
+               if (nodeId == null || !nodeId.equals(node.getIdentifier())
+                               || !property.equals(prop.getName())) {
+                       nodeId = node.getIdentifier();
+                       property = prop.getName();
+                       cache = node;
+                       itemUpdated();
+               } else {
+                       cache = node;// set/update cache
+               }
+       }
+
+       public synchronized String getNodeId() {
+               return nodeId;
+       }
+
+       /** Change the node, does nothing if same. */
+       public synchronized void setNodeId(String nodeId)
+                       throws RepositoryException {
+               if (this.nodeId != null && this.nodeId.equals(nodeId))
+                       return;
+               this.nodeId = nodeId;
+               if (cache != null)
+                       cache = session.getNodeByIdentifier(this.nodeId);
+               itemUpdated();
+       }
+
+       protected synchronized void itemUpdated() {
+               layout();
+       }
+
+       public Session getSession() {
+               return session;
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/ScrolledPage.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/ScrolledPage.java
new file mode 100644 (file)
index 0000000..c36ed20
--- /dev/null
@@ -0,0 +1,60 @@
+package org.argeo.cms.widgets;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * A composite that can be scrolled vertically. It wraps a
+ * {@link ScrolledComposite} (and is being wrapped by it), simplifying its
+ * configuration.
+ */
+public class ScrolledPage extends Composite {
+       private static final long serialVersionUID = 1593536965663574437L;
+
+       private ScrolledComposite scrolledComposite;
+
+       public ScrolledPage(Composite parent, int style) {
+               super(new ScrolledComposite(parent, SWT.V_SCROLL), style);
+               scrolledComposite = (ScrolledComposite) getParent();
+               scrolledComposite.setContent(this);
+
+               scrolledComposite.setExpandVertical(true);
+               scrolledComposite.setExpandHorizontal(true);
+               scrolledComposite.addControlListener(new ScrollControlListener());
+       }
+
+       @Override
+       public void layout(boolean changed, boolean all) {
+               updateScroll();
+               super.layout(changed, all);
+       }
+
+       protected void updateScroll() {
+               Rectangle r = scrolledComposite.getClientArea();
+               Point preferredSize = computeSize(r.width, SWT.DEFAULT);
+               scrolledComposite.setMinHeight(preferredSize.y);
+       }
+
+       // public ScrolledComposite getScrolledComposite() {
+       // return this.scrolledComposite;
+       // }
+
+       /** Set it on the wrapping scrolled composite */
+       @Override
+       public void setLayoutData(Object layoutData) {
+               scrolledComposite.setLayoutData(layoutData);
+       }
+
+       private class ScrollControlListener extends
+                       org.eclipse.swt.events.ControlAdapter {
+               private static final long serialVersionUID = -3586986238567483316L;
+
+               public void controlResized(ControlEvent e) {
+                       updateScroll();
+               }
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/StyledControl.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/StyledControl.java
new file mode 100644 (file)
index 0000000..2103189
--- /dev/null
@@ -0,0 +1,138 @@
+package org.argeo.cms.widgets;
+
+import javax.jcr.Item;
+import javax.jcr.RepositoryException;
+
+import org.argeo.cms.CmsNames;
+import org.argeo.cms.ui.CmsConstants;
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.MouseListener;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/** Editable text part displaying styled text. */
+public abstract class StyledControl extends JcrComposite implements
+               CmsConstants, CmsNames {
+       private static final long serialVersionUID = -6372283442330912755L;
+       private Control control;
+
+       private Composite container;
+       private Composite box;
+
+       protected MouseListener mouseListener;
+       protected FocusListener focusListener;
+
+       private Boolean editing = Boolean.FALSE;
+
+       public StyledControl(Composite parent, int swtStyle) {
+               super(parent, swtStyle);
+               setLayout(CmsUtils.noSpaceGridLayout());
+       }
+
+       public StyledControl(Composite parent, int style, Item item)
+                       throws RepositoryException {
+               super(parent, style, item);
+       }
+
+       public StyledControl(Composite parent, int style, Item item,
+                       boolean cacheImmediately) throws RepositoryException {
+               super(parent, style, item, cacheImmediately);
+       }
+
+       protected abstract Control createControl(Composite box, String style);
+
+       protected Composite createBox(Composite parent) {
+               Composite box = new Composite(parent, SWT.INHERIT_DEFAULT);
+               setContainerLayoutData(box);
+               box.setLayout(CmsUtils.noSpaceGridLayout());
+               // new Label(box, SWT.NONE).setText("BOX");
+               return box;
+       }
+
+       public Control getControl() {
+               return control;
+       }
+
+       protected synchronized Boolean isEditing() {
+               return editing;
+       }
+
+       public synchronized void startEditing() {
+               assert !isEditing();
+               editing = true;
+               // int height = control.getSize().y;
+               String style = (String) control.getData(STYLE);
+               clear(false);
+               control = createControl(box, style);
+               setControlLayoutData(control);
+
+               // add the focus listener to the newly created edition control
+               if (focusListener != null)
+                       control.addFocusListener(focusListener);
+       }
+
+       public synchronized void stopEditing() {
+               assert isEditing();
+               editing = false;
+               String style = (String) control.getData(STYLE);
+               clear(false);
+               control = createControl(box, style);
+               setControlLayoutData(control);
+       }
+
+       public void setStyle(String style) {
+               Object currentStyle = null;
+               if (control != null)
+                       currentStyle = control.getData(STYLE);
+               if (currentStyle != null && currentStyle.equals(style))
+                       return;
+
+               // Integer preferredHeight = control != null ? control.getSize().y :
+               // null;
+               clear(true);
+               control = createControl(box, style);
+               setControlLayoutData(control);
+
+               control.getParent().setData(STYLE, style + "_box");
+               control.getParent().getParent().setData(STYLE, style + "_container");
+       }
+
+       /** To be overridden */
+       protected void setControlLayoutData(Control control) {
+               control.setLayoutData(CmsUtils.fillWidth());
+       }
+
+       /** To be overridden */
+       protected void setContainerLayoutData(Composite composite) {
+               composite.setLayoutData(CmsUtils.fillWidth());
+       }
+
+       protected void clear(boolean deep) {
+               if (deep) {
+                       for (Control control : getChildren())
+                               control.dispose();
+                       container = createBox(this);
+                       box = createBox(container);
+               } else {
+                       control.dispose();
+               }
+       }
+
+       public void setMouseListener(MouseListener mouseListener) {
+               if (this.mouseListener != null && control != null)
+                       control.removeMouseListener(this.mouseListener);
+               this.mouseListener = mouseListener;
+               if (control != null && this.mouseListener != null)
+                       control.addMouseListener(mouseListener);
+       }
+
+       public void setFocusListener(FocusListener focusListener) {
+               if (this.focusListener != null && control != null)
+                       control.removeFocusListener(this.focusListener);
+               this.focusListener = focusListener;
+               if (control != null && this.focusListener != null)
+                       control.addFocusListener(focusListener);
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/AbstractLoginDialog.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/AbstractLoginDialog.java
new file mode 100644 (file)
index 0000000..b86fcb0
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.widgets.auth;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+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 org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.TrayDialog;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.operation.ModalContext;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.osgi.framework.FrameworkUtil;
+
+/** Base for login dialogs */
+public abstract class AbstractLoginDialog extends TrayDialog implements CallbackHandler {
+       private static final long serialVersionUID = -8046708963512717709L;
+
+       private final static Log log = LogFactory.getLog(AbstractLoginDialog.class);
+
+       private Thread modalContextThread = null;
+       boolean processCallbacks = false;
+       boolean isCancelled = false;
+       Callback[] callbackArray;
+
+       protected final Callback[] getCallbacks() {
+               return this.callbackArray;
+       }
+
+       public abstract void internalHandle();
+
+       public boolean isCancelled() {
+               return isCancelled;
+       }
+
+       protected AbstractLoginDialog(Shell parentShell) {
+               super(parentShell);
+       }
+
+       /*
+        * (non-Javadoc)
+        * 
+        * @see
+        * javax.security.auth.callback.CallbackHandler#handle(javax.security.auth
+        * .callback.Callback[])
+        */
+       public void handle(final Callback[] callbacks) throws IOException {
+               // clean previous usage
+               if (processCallbacks) {
+                       // this handler was already used
+                       processCallbacks = false;
+               }
+
+               if (modalContextThread != null) {
+                       try {
+                               modalContextThread.join(1000);
+                       } catch (InterruptedException e) {
+                               // silent
+                       }
+                       modalContextThread = null;
+               }
+
+               // initialize
+               this.callbackArray = callbacks;
+               final Display display = Display.getDefault();
+               display.syncExec(new Runnable() {
+
+                       public void run() {
+                               isCancelled = false;
+                               setBlockOnOpen(false);
+                               open();
+
+                               final Button okButton = getButton(IDialogConstants.OK_ID);
+                               okButton.setText("Login");
+                               okButton.addSelectionListener(new SelectionListener() {
+                                       private static final long serialVersionUID = -200281625679096775L;
+
+                                       public void widgetSelected(final SelectionEvent event) {
+                                               processCallbacks = true;
+                                       }
+
+                                       public void widgetDefaultSelected(final SelectionEvent event) {
+                                               // nothing to do
+                                       }
+                               });
+                               final Button cancel = getButton(IDialogConstants.CANCEL_ID);
+                               cancel.addSelectionListener(new SelectionListener() {
+                                       private static final long serialVersionUID = -3826030278084915815L;
+
+                                       public void widgetSelected(final SelectionEvent event) {
+                                               isCancelled = true;
+                                               processCallbacks = true;
+                                       }
+
+                                       public void widgetDefaultSelected(final SelectionEvent event) {
+                                               // nothing to do
+                                       }
+                               });
+                       }
+               });
+               try {
+                       ModalContext.setAllowReadAndDispatch(true); // Works for now.
+                       ModalContext.run(new IRunnableWithProgress() {
+
+                               public void run(final IProgressMonitor monitor) {
+                                       modalContextThread = Thread.currentThread();
+                                       // Wait here until OK or cancel is pressed, then let it rip.
+                                       // The event
+                                       // listener
+                                       // is responsible for closing the dialog (in the
+                                       // loginSucceeded
+                                       // event).
+                                       while (!processCallbacks && (modalContextThread != null)
+                                                       && (modalContextThread == Thread.currentThread())
+                                                       && FrameworkUtil.getBundle(AbstractLoginDialog.class).getBundleContext() != null) {
+                                               // Note: SecurityUiPlugin.getDefault() != null is false
+                                               // when the OSGi runtime is shut down
+                                               try {
+                                                       Thread.sleep(100);
+                                                       // if (display.isDisposed()) {
+                                                       // log.warn("Display is disposed, killing login
+                                                       // dialog thread");
+                                                       // throw new ThreadDeath();
+                                                       // }
+                                               } catch (final Exception e) {
+                                                       // do nothing
+                                               }
+                                       }
+                                       processCallbacks = false;
+                                       // Call the adapter to handle the callbacks
+                                       if (!isCancelled())
+                                               internalHandle();
+                                       else
+                                               // clear callbacks are when cancelling
+                                               for (Callback callback : callbacks)
+                                                       if (callback instanceof PasswordCallback) {
+                                                               char[] arr = ((PasswordCallback) callback).getPassword();
+                                                               if (arr != null) {
+                                                                       Arrays.fill(arr, '*');
+                                                                       ((PasswordCallback) callback).setPassword(null);
+                                                               }
+                                                       } else if (callback instanceof NameCallback)
+                                                               ((NameCallback) callback).setName(null);
+                               }
+                       }, true, new NullProgressMonitor(), Display.getDefault());
+               } catch (ThreadDeath e) {
+                       isCancelled = true;
+                       log.debug("Thread " + Thread.currentThread().getId() + " died");
+                       throw e;
+               } catch (Exception e) {
+                       isCancelled = true;
+                       IOException ioe = new IOException("Unexpected issue in login dialog, see root cause for more details");
+                       ioe.initCause(e);
+                       throw ioe;
+               } finally {
+                       // so that the modal thread dies
+                       processCallbacks = true;
+                       // try {
+                       // // wait for the modal context thread to gracefully exit
+                       // modalContextThread.join();
+                       // } catch (InterruptedException ie) {
+                       // // silent
+                       // }
+                       modalContextThread = null;
+               }
+       }
+
+       protected void configureShell(Shell shell) {
+               super.configureShell(shell);
+               shell.setText("Authentication");
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CmsLogin.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CmsLogin.java
new file mode 100644 (file)
index 0000000..7560ceb
--- /dev/null
@@ -0,0 +1,324 @@
+package org.argeo.cms.widgets.auth;
+
+import static org.argeo.cms.CmsMsg.password;
+import static org.argeo.cms.CmsMsg.username;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.LanguageCallback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsMsg;
+import org.argeo.cms.auth.HttpRequestCallback;
+import org.argeo.cms.i18n.LocaleUtils;
+import org.argeo.cms.ui.CmsStyles;
+import org.argeo.cms.ui.CmsView;
+import org.argeo.cms.ui.internal.Activator;
+import org.argeo.cms.util.CmsUtils;
+import org.argeo.eclipse.ui.specific.UiContext;
+import org.argeo.node.NodeConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.events.TraverseEvent;
+import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+public class CmsLogin implements CmsStyles, CallbackHandler {
+       private final static Log log = LogFactory.getLog(CmsLogin.class);
+
+       private Composite parent;
+       private Text usernameT, passwordT;
+       private Composite credentialsBlock;
+       private final SelectionListener loginSelectionListener;
+
+       private final Locale defaultLocale;
+       private LocaleChoice localeChoice = null;
+
+       private final CmsView cmsView;
+
+       // optional subject to be set explicitly
+       private Subject subject = null;
+
+       public CmsLogin(CmsView cmsView) {
+               this.cmsView = cmsView;
+               defaultLocale = Activator.getNodeState().getDefaultLocale();
+               List<Locale> locales = Activator.getNodeState().getLocales();
+               if (locales != null)
+                       localeChoice = new LocaleChoice(locales, defaultLocale);
+               loginSelectionListener = new SelectionListener() {
+                       private static final long serialVersionUID = -8832133363830973578L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               login();
+                       }
+
+                       @Override
+                       public void widgetDefaultSelected(SelectionEvent e) {
+                       }
+               };
+       }
+
+       protected boolean isAnonymous() {
+               return cmsView.isAnonymous();
+       }
+
+       public final void createUi(Composite parent) {
+               this.parent = parent;
+               createContents(parent);
+       }
+
+       protected void createContents(Composite parent) {
+               defaultCreateContents(parent);
+       }
+
+       public final void defaultCreateContents(Composite parent) {
+               parent.setLayout(CmsUtils.noSpaceGridLayout());
+               Composite credentialsBlock = createCredentialsBlock(parent);
+               if (parent instanceof Shell) {
+                       credentialsBlock.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
+               }
+       }
+
+       public final Composite createCredentialsBlock(Composite parent) {
+               if (isAnonymous()) {
+                       return anonymousUi(parent);
+               } else {
+                       return userUi(parent);
+               }
+       }
+
+       protected Composite getCredentialsBlock() {
+               return credentialsBlock;
+       }
+
+       protected Composite userUi(Composite parent) {
+               Locale locale = localeChoice == null ? this.defaultLocale : localeChoice.getSelectedLocale();
+               credentialsBlock = new Composite(parent, SWT.NONE);
+               credentialsBlock.setLayout(new GridLayout());
+               // credentialsBlock.setLayoutData(CmsUtils.fillAll());
+
+               specificUserUi(credentialsBlock);
+
+               Label l = new Label(credentialsBlock, SWT.NONE);
+               CmsUtils.style(l, CMS_USER_MENU_ITEM);
+               l.setText(CmsMsg.logout.lead(locale));
+               GridData lData = CmsUtils.fillWidth();
+               lData.widthHint = 120;
+               l.setLayoutData(lData);
+
+               l.addMouseListener(new MouseAdapter() {
+                       private static final long serialVersionUID = 6444395812777413116L;
+
+                       public void mouseDown(MouseEvent e) {
+                               logout();
+                       }
+               });
+               return credentialsBlock;
+       }
+
+       /** To be overridden */
+       protected void specificUserUi(Composite parent) {
+
+       }
+
+       protected Composite anonymousUi(Composite parent) {
+               Locale locale = localeChoice == null ? this.defaultLocale : localeChoice.getSelectedLocale();
+               // We need a composite for the traversal
+               credentialsBlock = new Composite(parent, SWT.NONE);
+               credentialsBlock.setLayout(new GridLayout());
+               // credentialsBlock.setLayoutData(CmsUtils.fillAll());
+               CmsUtils.style(credentialsBlock, CMS_LOGIN_DIALOG);
+
+               Integer textWidth = 120;
+               if (parent instanceof Shell)
+                       CmsUtils.style(parent, CMS_USER_MENU);
+               // new Label(this, SWT.NONE).setText(CmsMsg.username.lead());
+               usernameT = new Text(credentialsBlock, SWT.BORDER);
+               usernameT.setMessage(username.lead(locale));
+               CmsUtils.style(usernameT, CMS_LOGIN_DIALOG_USERNAME);
+               GridData gd = CmsUtils.fillWidth();
+               gd.widthHint = textWidth;
+               usernameT.setLayoutData(gd);
+
+               // new Label(this, SWT.NONE).setText(CmsMsg.password.lead());
+               passwordT = new Text(credentialsBlock, SWT.BORDER | SWT.PASSWORD);
+               passwordT.setMessage(password.lead(locale));
+               CmsUtils.style(passwordT, CMS_LOGIN_DIALOG_PASSWORD);
+               gd = CmsUtils.fillWidth();
+               gd.widthHint = textWidth;
+               passwordT.setLayoutData(gd);
+
+               TraverseListener tl = new TraverseListener() {
+                       private static final long serialVersionUID = -1158892811534971856L;
+
+                       public void keyTraversed(TraverseEvent e) {
+                               if (e.detail == SWT.TRAVERSE_RETURN)
+                                       login();
+                       }
+               };
+               credentialsBlock.addTraverseListener(tl);
+               usernameT.addTraverseListener(tl);
+               passwordT.addTraverseListener(tl);
+               parent.setTabList(new Control[] { credentialsBlock });
+               credentialsBlock.setTabList(new Control[] { usernameT, passwordT });
+               // credentialsBlock.setFocus();
+
+               extendsCredentialsBlock(credentialsBlock, locale, loginSelectionListener);
+               if (localeChoice != null)
+                       createLocalesBlock(credentialsBlock);
+               return credentialsBlock;
+       }
+
+       /**
+        * To be overridden in order to provide custom login button and other links.
+        */
+       protected void extendsCredentialsBlock(Composite credentialsBlock, Locale selectedLocale,
+                       SelectionListener loginSelectionListener) {
+
+       }
+
+       protected void updateLocale(Locale selectedLocale) {
+               // save already entered values
+               String usernameStr = usernameT.getText();
+               char[] pwd = passwordT.getTextChars();
+
+               for (Control child : parent.getChildren())
+                       child.dispose();
+               createContents(parent);
+               if (parent.getParent() != null)
+                       parent.getParent().layout();
+               else
+                       parent.layout();
+               usernameT.setText(usernameStr);
+               passwordT.setTextChars(pwd);
+       }
+
+       protected Composite createLocalesBlock(final Composite parent) {
+               Composite c = new Composite(parent, SWT.NONE);
+               CmsUtils.style(c, CMS_USER_MENU_ITEM);
+               c.setLayout(CmsUtils.noSpaceGridLayout());
+               c.setLayoutData(CmsUtils.fillAll());
+
+               SelectionListener selectionListener = new SelectionAdapter() {
+                       private static final long serialVersionUID = 4891637813567806762L;
+
+                       public void widgetSelected(SelectionEvent event) {
+                               Button button = (Button) event.widget;
+                               if (button.getSelection()) {
+                                       localeChoice.setSelectedIndex((Integer) event.widget.getData());
+                                       updateLocale(localeChoice.getSelectedLocale());
+                               }
+                       };
+               };
+
+               List<Locale> locales = localeChoice.getLocales();
+               for (Integer i = 0; i < locales.size(); i++) {
+                       Locale locale = locales.get(i);
+                       Button button = new Button(c, SWT.RADIO);
+                       CmsUtils.style(button, CMS_USER_MENU_ITEM);
+                       button.setData(i);
+                       button.setText(LocaleUtils.lead(locale.getDisplayName(locale), locale) + " (" + locale + ")");
+                       // button.addListener(SWT.Selection, listener);
+                       button.addSelectionListener(selectionListener);
+                       if (i == localeChoice.getSelectedIndex())
+                               button.setSelection(true);
+               }
+               return c;
+       }
+
+       protected boolean login() {
+               // TODO use CmsVie in order to retrieve subject?
+               // Subject subject = cmsView.getLoginContext().getSubject();
+               // LoginContext loginContext = cmsView.getLoginContext();
+               try {
+                       //
+                       // LOGIN
+                       //
+                       // loginContext.logout();
+                       LoginContext loginContext;
+                       if (subject == null)
+                               loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, this);
+                       else
+                               loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, subject, this);
+                       loginContext.login();
+                       cmsView.authChange(loginContext);
+                       return true;
+               } catch (LoginException e) {
+                       if (log.isTraceEnabled())
+                               log.warn("Login failed: " + e.getMessage(), e);
+                       else
+                               log.warn("Login failed: " + e.getMessage());
+
+                       try {
+                               Thread.sleep(3000);
+                       } catch (InterruptedException e2) {
+                               // silent
+                       }
+                       // ErrorFeedback.show("Login failed", e);
+                       return false;
+               }
+               // catch (LoginException e) {
+               // log.error("Cannot login", e);
+               // return false;
+               // }
+       }
+
+       protected void logout() {
+               cmsView.logout();
+               cmsView.navigateTo("~");
+       }
+
+       @Override
+       public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+               for (Callback callback : callbacks) {
+                       if (callback instanceof NameCallback && usernameT != null)
+                               ((NameCallback) callback).setName(usernameT.getText());
+                       else if (callback instanceof PasswordCallback && passwordT != null)
+                               ((PasswordCallback) callback).setPassword(passwordT.getTextChars());
+                       else if (callback instanceof HttpRequestCallback) {
+                               ((HttpRequestCallback) callback).setRequest(UiContext.getHttpRequest());
+                               ((HttpRequestCallback) callback).setResponse(UiContext.getHttpResponse());
+                       } else if (callback instanceof LanguageCallback) {
+                               Locale toUse = null;
+                               if (localeChoice != null)
+                                       toUse = localeChoice.getSelectedLocale();
+                               else if (defaultLocale != null)
+                                       toUse = defaultLocale;
+
+                               if (toUse != null) {
+                                       ((LanguageCallback) callback).setLocale(toUse);
+                                       UiContext.setLocale(toUse);
+                               }
+
+                       }
+               }
+       }
+
+       public void setSubject(Subject subject) {
+               this.subject = subject;
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CmsLoginShell.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CmsLoginShell.java
new file mode 100644 (file)
index 0000000..dea632d
--- /dev/null
@@ -0,0 +1,72 @@
+package org.argeo.cms.widgets.auth;
+
+import org.argeo.cms.ui.CmsView;
+import org.argeo.cms.util.CmsUtils;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+/** The site-related user menu */
+public class CmsLoginShell extends CmsLogin {
+       private final Shell shell;
+
+       public CmsLoginShell(CmsView cmsView) {
+               super(cmsView);
+               shell = createShell();
+               CmsUtils.style(shell, CMS_USER_MENU);
+//             createUi(shell);
+       }
+
+       /** To be overridden. */
+       protected Shell createShell() {
+               Shell shell = new Shell(Display.getCurrent(), SWT.NO_TRIM);
+               shell.setMaximized(true);
+               return shell;
+       }
+
+       /** To be overridden. */
+       public void open() {
+               shell.open();
+       }
+
+       @Override
+       protected boolean login() {
+               boolean success = false;
+               try {
+                       success = super.login();
+                       return success;
+               } finally {
+                       if (success)
+                               closeShell();
+                       else {
+                               for (Control child : shell.getChildren())
+                                       child.dispose();
+                               createUi(shell);
+                               shell.layout();
+                               // TODO error message
+                       }
+               }
+       }
+
+       @Override
+       protected void logout() {
+               closeShell();
+               super.logout();
+       }
+
+       protected void closeShell() {
+               if (!shell.isDisposed()) {
+                       shell.close();
+                       shell.dispose();
+               }
+       }
+
+       public Shell getShell() {
+               return shell;
+       }
+       
+       public void createUi(){
+               createUi(shell);
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CompositeCallbackHandler.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/CompositeCallbackHandler.java
new file mode 100644 (file)
index 0000000..1f72e23
--- /dev/null
@@ -0,0 +1,273 @@
+package org.argeo.cms.widgets.auth;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+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.TextOutputCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * A composite that can populate itself based on {@link Callback}s. It can be
+ * used directly as a {@link CallbackHandler} or be used by one by calling the
+ * {@link #createCallbackHandlers(Callback[])}. Supported standard
+ * {@link Callback}s are:<br>
+ * <ul>
+ * <li>{@link PasswordCallback}</li>
+ * <li>{@link NameCallback}</li>
+ * <li>{@link TextOutputCallback}</li>
+ * </ul>
+ * Supported Argeo {@link Callback}s are:<br>
+ * <ul>
+ * <li>{@link LocaleChoice}</li>
+ * </ul>
+ */
+public class CompositeCallbackHandler extends Composite implements CallbackHandler {
+       private static final long serialVersionUID = -928223893722723777L;
+
+       private boolean wasUsedAlready = false;
+       private boolean isSubmitted = false;
+       private boolean isCanceled = false;
+
+       public CompositeCallbackHandler(Composite parent, int style) {
+               super(parent, style);
+       }
+
+       @Override
+       public synchronized void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+               // reset
+               if (wasUsedAlready && !isSubmitted() && !isCanceled()) {
+                       cancel();
+                       for (Control control : getChildren())
+                               control.dispose();
+                       isSubmitted = false;
+                       isCanceled = false;
+               }
+
+               for (Callback callback : callbacks)
+                       checkCallbackSupported(callback);
+               // create controls synchronously in the UI thread
+               getDisplay().syncExec(new Runnable() {
+
+                       @Override
+                       public void run() {
+                               createCallbackHandlers(callbacks);
+                       }
+               });
+
+               if (!wasUsedAlready)
+                       wasUsedAlready = true;
+
+               // while (!isSubmitted() && !isCanceled()) {
+               // try {
+               // wait(1000l);
+               // } catch (InterruptedException e) {
+               // // silent
+               // }
+               // }
+
+               // cleanCallbacksAfterCancel(callbacks);
+       }
+
+       public void checkCallbackSupported(Callback callback) throws UnsupportedCallbackException {
+               if (callback instanceof TextOutputCallback || callback instanceof NameCallback
+                               || callback instanceof PasswordCallback || callback instanceof LocaleChoice) {
+                       return;
+               } else {
+                       throw new UnsupportedCallbackException(callback);
+               }
+       }
+
+       /**
+        * Set writable callbacks to null if the handle is canceled (check is done
+        * by the method)
+        */
+       public void cleanCallbacksAfterCancel(Callback[] callbacks) {
+               if (isCanceled()) {
+                       for (Callback callback : callbacks) {
+                               if (callback instanceof NameCallback) {
+                                       ((NameCallback) callback).setName(null);
+                               } else if (callback instanceof PasswordCallback) {
+                                       PasswordCallback pCallback = (PasswordCallback) callback;
+                                       char[] arr = pCallback.getPassword();
+                                       if (arr != null) {
+                                               Arrays.fill(arr, '*');
+                                               pCallback.setPassword(null);
+                                       }
+                               }
+                       }
+               }
+       }
+
+       public void createCallbackHandlers(Callback[] callbacks) {
+               Composite composite = this;
+               for (int i = 0; i < callbacks.length; i++) {
+                       Callback callback = callbacks[i];
+                       if (callback instanceof TextOutputCallback) {
+                               createLabelTextoutputHandler(composite, (TextOutputCallback) callback);
+                       } else if (callback instanceof NameCallback) {
+                               createNameHandler(composite, (NameCallback) callback);
+                       } else if (callback instanceof PasswordCallback) {
+                               createPasswordHandler(composite, (PasswordCallback) callback);
+                       } else if (callback instanceof LocaleChoice) {
+                               createLocaleHandler(composite, (LocaleChoice) callback);
+                       }
+               }
+       }
+
+       protected Text createNameHandler(Composite composite, final NameCallback callback) {
+               Label label = new Label(composite, SWT.NONE);
+               label.setText(callback.getPrompt());
+               final Text text = new Text(composite, SWT.SINGLE | SWT.LEAD | SWT.BORDER);
+               if (callback.getDefaultName() != null) {
+                       // set default value, if provided
+                       text.setText(callback.getDefaultName());
+                       callback.setName(callback.getDefaultName());
+               }
+               text.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               text.addModifyListener(new ModifyListener() {
+                       private static final long serialVersionUID = 7300032545287292973L;
+
+                       public void modifyText(ModifyEvent event) {
+                               callback.setName(text.getText());
+                       }
+               });
+               text.addSelectionListener(new SelectionListener() {
+                       private static final long serialVersionUID = 1820530045857665111L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                       }
+
+                       @Override
+                       public void widgetDefaultSelected(SelectionEvent e) {
+                               submit();
+                       }
+               });
+
+               text.addKeyListener(new KeyListener() {
+                       private static final long serialVersionUID = -8698107785092095713L;
+
+                       @Override
+                       public void keyReleased(KeyEvent e) {
+                       }
+
+                       @Override
+                       public void keyPressed(KeyEvent e) {
+                       }
+               });
+               return text;
+       }
+
+       protected Text createPasswordHandler(Composite composite, final PasswordCallback callback) {
+               Label label = new Label(composite, SWT.NONE);
+               label.setText(callback.getPrompt());
+               final Text passwordText = new Text(composite, SWT.SINGLE | SWT.LEAD | SWT.PASSWORD | SWT.BORDER);
+               passwordText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               passwordText.addModifyListener(new ModifyListener() {
+                       private static final long serialVersionUID = -7099363995047686732L;
+
+                       public void modifyText(ModifyEvent event) {
+                               callback.setPassword(passwordText.getTextChars());
+                       }
+               });
+               passwordText.addSelectionListener(new SelectionListener() {
+                       private static final long serialVersionUID = 1820530045857665111L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                       }
+
+                       @Override
+                       public void widgetDefaultSelected(SelectionEvent e) {
+                               submit();
+                       }
+               });
+               return passwordText;
+       }
+
+       protected Combo createLocaleHandler(Composite composite, final LocaleChoice callback) {
+               String[] labels = callback.getSupportedLocalesLabels();
+               if (labels.length == 0)
+                       return null;
+               Label label = new Label(composite, SWT.NONE);
+               label.setText("Language");
+
+               final Combo combo = new Combo(composite, SWT.READ_ONLY);
+               combo.setItems(labels);
+               combo.select(callback.getDefaultIndex());
+               combo.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               combo.addSelectionListener(new SelectionListener() {
+                       private static final long serialVersionUID = 38678989091946277L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               callback.setSelectedIndex(combo.getSelectionIndex());
+                       }
+
+                       @Override
+                       public void widgetDefaultSelected(SelectionEvent e) {
+                       }
+               });
+               return combo;
+       }
+
+       protected Label createLabelTextoutputHandler(Composite composite, final TextOutputCallback callback) {
+               Label label = new Label(composite, SWT.NONE);
+               label.setText(callback.getMessage());
+               GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
+               data.horizontalSpan = 2;
+               label.setLayoutData(data);
+               return label;
+               // TODO: find a way to pass this information
+               // int messageType = callback.getMessageType();
+               // int dialogMessageType = IMessageProvider.NONE;
+               // switch (messageType) {
+               // case TextOutputCallback.INFORMATION:
+               // dialogMessageType = IMessageProvider.INFORMATION;
+               // break;
+               // case TextOutputCallback.WARNING:
+               // dialogMessageType = IMessageProvider.WARNING;
+               // break;
+               // case TextOutputCallback.ERROR:
+               // dialogMessageType = IMessageProvider.ERROR;
+               // break;
+               // }
+               // setMessage(callback.getMessage(), dialogMessageType);
+       }
+
+       synchronized boolean isSubmitted() {
+               return isSubmitted;
+       }
+
+       synchronized boolean isCanceled() {
+               return isCanceled;
+       }
+
+       protected synchronized void submit() {
+               isSubmitted = true;
+               notifyAll();
+       }
+
+       protected synchronized void cancel() {
+               isCanceled = true;
+               notifyAll();
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/DefaultLoginDialog.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/DefaultLoginDialog.java
new file mode 100644 (file)
index 0000000..b8de34b
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.widgets.auth;
+
+import javax.security.auth.callback.CallbackHandler;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+/** Default authentication dialog, to be used as {@link CallbackHandler}. */
+public class DefaultLoginDialog extends AbstractLoginDialog {
+       private static final long serialVersionUID = -8551827590693035734L;
+
+       public DefaultLoginDialog() {
+               this(Display.getCurrent().getActiveShell());
+       }
+
+       public DefaultLoginDialog(Shell parentShell) {
+               super(parentShell);
+       }
+
+       protected Point getInitialSize() {
+               return new Point(350, 180);
+       }
+
+       @Override
+       protected Control createContents(Composite parent) {
+               Control control = super.createContents(parent);
+               parent.pack();
+
+               // Move the dialog to the center of the top level shell.
+               Rectangle shellBounds;
+               if (Display.getCurrent().getActiveShell() != null) // RCP
+                       shellBounds = Display.getCurrent().getActiveShell().getBounds();
+               else
+                       shellBounds = Display.getCurrent().getBounds();// RAP
+               Point dialogSize = parent.getSize();
+               int x = shellBounds.x + (shellBounds.width - dialogSize.x) / 2;
+               int y = shellBounds.y + (shellBounds.height - dialogSize.y) / 2;
+               parent.setLocation(x, y);
+               return control;
+       }
+
+       protected Control createDialogArea(Composite parent) {
+               Composite dialogarea = (Composite) super.createDialogArea(parent);
+               CompositeCallbackHandler composite = new CompositeCallbackHandler(
+                               dialogarea, SWT.NONE);
+               composite.setLayout(new GridLayout(2, false));
+               composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+               composite.createCallbackHandlers(getCallbacks());
+               return composite;
+       }
+
+       public void internalHandle() {
+       }
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/DynamicCallbackHandler.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/DynamicCallbackHandler.java
new file mode 100644 (file)
index 0000000..b206355
--- /dev/null
@@ -0,0 +1,34 @@
+package org.argeo.cms.widgets.auth;
+
+import java.io.IOException;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import org.argeo.eclipse.ui.dialogs.LightweightDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+public class DynamicCallbackHandler implements CallbackHandler {
+
+       @Override
+       public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+               Shell activeShell = Display.getCurrent().getActiveShell();
+               LightweightDialog dialog = new LightweightDialog(activeShell) {
+
+                       @Override
+                       protected Control createDialogArea(Composite parent) {
+                               CompositeCallbackHandler cch = new CompositeCallbackHandler(parent, SWT.NONE);
+                               cch.createCallbackHandlers(callbacks);
+                               return cch;
+                       }
+               };
+               dialog.setBlockOnOpen(true);
+               dialog.open();
+       }
+
+}
diff --git a/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/LocaleChoice.java b/org.argeo.cms.ui/src/org/argeo/cms/widgets/auth/LocaleChoice.java
new file mode 100644 (file)
index 0000000..009c372
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.widgets.auth;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import javax.security.auth.callback.LanguageCallback;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.i18n.LocaleUtils;
+
+/** Choose in a list of locales. TODO: replace with {@link LanguageCallback} */
+public class LocaleChoice {
+       private final List<Locale> locales;
+
+       private Integer selectedIndex = null;
+       private final Integer defaultIndex;
+
+       public LocaleChoice(List<Locale> locales, Locale defaultLocale) {
+               Integer defaultIndex = null;
+               this.locales = Collections.unmodifiableList(locales);
+               for (int i = 0; i < locales.size(); i++)
+                       if (locales.get(i).equals(defaultLocale))
+                               defaultIndex = i;
+
+               // based on language only
+               if (defaultIndex == null)
+                       for (int i = 0; i < locales.size(); i++)
+                               if (locales.get(i).getLanguage().equals(defaultLocale.getLanguage()))
+                                       defaultIndex = i;
+
+               if (defaultIndex == null)
+                       throw new CmsException("Default locale " + defaultLocale + " is not in available locales " + locales);
+               this.defaultIndex = defaultIndex;
+
+               this.selectedIndex = defaultIndex;
+       }
+
+       /**
+        * Convenience constructor based on a comma separated list of iso codes (en,
+        * en_US, fr_CA, etc.). Default selection is default locale.
+        */
+       public LocaleChoice(String locales, Locale defaultLocale) {
+               this(LocaleUtils.asLocaleList(locales), defaultLocale);
+       }
+
+       public String[] getSupportedLocalesLabels() {
+               String[] labels = new String[locales.size()];
+               for (int i = 0; i < locales.size(); i++) {
+                       Locale locale = locales.get(i);
+                       if (locale.getCountry().equals(""))
+                               labels[i] = locale.getDisplayLanguage(locale) + " [" + locale.getLanguage() + "]";
+                       else
+                               labels[i] = locale.getDisplayLanguage(locale) + " (" + locale.getDisplayCountry(locale) + ") ["
+                                               + locale.getLanguage() + "_" + locale.getCountry() + "]";
+
+               }
+               return labels;
+       }
+
+       public Locale getSelectedLocale() {
+               if (selectedIndex == null)
+                       return null;
+               return locales.get(selectedIndex);
+       }
+
+       public void setSelectedIndex(Integer selectedIndex) {
+               this.selectedIndex = selectedIndex;
+       }
+
+       public Integer getSelectedIndex() {
+               return selectedIndex;
+       }
+
+       public Integer getDefaultIndex() {
+               return defaultIndex;
+       }
+
+       public List<Locale> getLocales() {
+               return locales;
+       }
+
+       public Locale getDefaultLocale() {
+               return locales.get(getDefaultIndex());
+       }
+}
diff --git a/org.argeo.cms/.classpath b/org.argeo.cms/.classpath
new file mode 100644 (file)
index 0000000..53c7dca
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="src" path="ext/test"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.cms/.gitignore b/org.argeo.cms/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.cms/.project b/org.argeo.cms/.project
new file mode 100644 (file)
index 0000000..2e18c90
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.cms/META-INF/.gitignore b/org.argeo.cms/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.cms/OSGI-INF/l10n/bundle.properties b/org.argeo.cms/OSGI-INF/l10n/bundle.properties
new file mode 100644 (file)
index 0000000..20d5b10
--- /dev/null
@@ -0,0 +1,19 @@
+username=username
+password=password
+logout=sign out
+login=sign in
+register=register
+
+changePassword=Change password
+currentPassword=Current password
+newPassword=New password
+repeatNewPassword=Repeat new password
+passwordChanged=Password changed
+
+close=Close
+cancel=Cancel
+ok=Ok
+
+wizardBack=Back
+wizardNext=Next
+wizardFinish=Finish
diff --git a/org.argeo.cms/OSGI-INF/l10n/bundle_ar.properties b/org.argeo.cms/OSGI-INF/l10n/bundle_ar.properties
new file mode 100644 (file)
index 0000000..2036e0f
--- /dev/null
@@ -0,0 +1,19 @@
+username=\u0645\u0633\u062A\u062E\u062F\u0645
+password=\u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631
+logout=sign out
+login=sign in
+register=register
+
+changePassword=Change password
+currentPassword=Current password
+newPassword=New password
+repeatNewPassword=Repeat new password
+passwordChanged=Password changed
+
+close=Close
+cancel=Cancel
+ok=Ok
+
+wizardBack=Back
+wizardNext=Next
+wizardFinish=Finish
diff --git a/org.argeo.cms/OSGI-INF/l10n/bundle_de.properties b/org.argeo.cms/OSGI-INF/l10n/bundle_de.properties
new file mode 100644 (file)
index 0000000..93a3d71
--- /dev/null
@@ -0,0 +1,19 @@
+username=Benutzer
+password=Passwort
+logout=ausloggen
+login=einloggen
+register=registrieren
+
+changePassword=Passwort ändern
+currentPassword=Derzeites Passwort
+newPassword=Neues passwort
+repeatNewPassword=Newes Passwort wiederholen
+passwordChanged=Passwort geändert
+
+close=Schließen
+cancel=Abbrechen
+ok=Ok
+
+wizardBack=Zurück
+wizardNext=Nächstes
+wizardFinish=Fertig
diff --git a/org.argeo.cms/OSGI-INF/l10n/bundle_fr.properties b/org.argeo.cms/OSGI-INF/l10n/bundle_fr.properties
new file mode 100644 (file)
index 0000000..085ea74
--- /dev/null
@@ -0,0 +1,19 @@
+username=identifiant
+password=mot de passe
+logout=déconnexion
+login=connexion
+register=créer un compte
+
+changePassword=changement de mot de passe
+currentPassword=mot de passe actuel
+newPassword=nouveau mot de passe
+repeatNewPassword=répéter le nouveau mot de passe
+passwordChanged=mot de passe changé
+
+close=Fermer
+cancel=Annuler
+ok=Ok
+
+wizardBack=Précédent
+wizardNext=Suivant
+wizardFinish=Terminer
diff --git a/org.argeo.cms/OSGI-INF/l10n/bundle_ru.properties b/org.argeo.cms/OSGI-INF/l10n/bundle_ru.properties
new file mode 100644 (file)
index 0000000..5cacae4
--- /dev/null
@@ -0,0 +1,11 @@
+username=\u0438\u043C\u044F
+password=\u043F\u0430\u0440\u043E\u043B\u044C
+logout=\u0432\u044B\u0439\u0442\u0438
+login=\u0432\u043E\u0439\u0442\u0438
+register=créer un compte
+
+changePassword=\u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C
+currentPassword=mot de passe actuel
+newPassword=nouveau mot de passe
+repeatNewPassword=répéter le nouveau mot de passe
+passwordChanged=mot de passe changé
diff --git a/org.argeo.cms/bnd.bnd b/org.argeo.cms/bnd.bnd
new file mode 100644 (file)
index 0000000..3061a0e
--- /dev/null
@@ -0,0 +1,15 @@
+Bundle-SymbolicName: org.argeo.cms;singleton:=true
+Bundle-Activator: org.argeo.cms.internal.kernel.Activator
+Import-Package: javax.jcr.security,\
+org.h2;resolution:=optional,\
+org.postgresql;resolution:=optional,\
+org.apache.jackrabbit.webdav.server,\
+org.apache.jackrabbit.webdav.jcr,\
+org.springframework.context;resolution:=optional,\
+org.springframework.core.io;resolution:=optional,\
+org.springframework.*;resolution:=optional,\
+org.eclipse.gemini.blueprint.*;resolution:=optional\
+org.apache.commons.httpclient.cookie;resolution:=optional,\
+org.osgi.*;version=0.0.0,\
+*
+Provide-Capability: cms.datamodel;name=cms;cnd=/org/argeo/cms/cms.cnd;abstract=true
\ No newline at end of file
diff --git a/org.argeo.cms/build.properties b/org.argeo.cms/build.properties
new file mode 100644 (file)
index 0000000..e8ee671
--- /dev/null
@@ -0,0 +1,10 @@
+output.. = bin/
+bin.includes = META-INF/,\
+               .,\
+               OSGI-INF/,\
+               bin/,\
+               OSGI-INF/org.argeo.cms.internal.kernel.KernelInitOld.xml
+source.. = src/
+additional.bundles = org.apache.jackrabbit.data,\
+                     org.argeo.jcr,\
+                     org.junit
diff --git a/org.argeo.cms/ext/test/org/argeo/cms/security/PasswordBasedEncryptionTest.java b/org.argeo.cms/ext/test/org/argeo/cms/security/PasswordBasedEncryptionTest.java
new file mode 100644 (file)
index 0000000..5c43e34
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.security;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+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.PBEParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.xml.bind.DatatypeConverter;
+
+import junit.framework.TestCase;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.util.PasswordEncryption;
+
+public class PasswordBasedEncryptionTest extends TestCase {
+       private final static Log log = LogFactory
+                       .getLog(PasswordBasedEncryptionTest.class);
+
+       public void testEncryptDecrypt() {
+               final String password = "test long password since they are safer";
+               PasswordEncryption pbeEnc = new PasswordEncryption(
+                               password.toCharArray());
+               String message = "Hello World!";
+               log.info("Password:\t'" + password + "'");
+               log.info("Message:\t'" + message + "'");
+               byte[] encrypted = pbeEnc.encryptString(message);
+               log.info("Encrypted:\t'"
+                               + DatatypeConverter.printBase64Binary(encrypted) + "'");
+               PasswordEncryption pbeDec = new PasswordEncryption(
+                               password.toCharArray());
+               InputStream in = null;
+               in = new ByteArrayInputStream(encrypted);
+               String decrypted = pbeDec.decryptAsString(in);
+               log.info("Decrypted:\t'" + decrypted + "'");
+               IOUtils.closeQuietly(in);
+               assertEquals(message, decrypted);
+       }
+
+       public void testPBEWithMD5AndDES() throws Exception {
+               String password = "test";
+               String message = "Hello World!";
+
+               byte[] salt = { (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c,
+                               (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99 };
+
+               int count = 1024;
+
+               String cipherAlgorithm = "PBEWithMD5AndDES";
+               String secretKeyAlgorithm = "PBEWithMD5AndDES";
+               SecretKeyFactory keyFac = SecretKeyFactory
+                               .getInstance(secretKeyAlgorithm);
+               PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
+               PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count);
+               SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec);
+               Cipher ecipher = Cipher.getInstance(cipherAlgorithm);
+               ecipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);
+               Cipher dcipher = Cipher.getInstance(cipherAlgorithm);
+               dcipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec);
+
+               byte[] encrypted = ecipher.doFinal(message.getBytes());
+               byte[] decrypted = dcipher.doFinal(encrypted);
+               assertEquals(message, new String(decrypted));
+
+       }
+
+       public void testPBEWithSHA1AndAES() throws Exception {
+               String password = "test";
+               String message = "Hello World!";
+
+               byte[] salt = { (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c,
+                               (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99 };
+               byte[] iv = { (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c,
+                               (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99,
+                               (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c,
+                               (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99 };
+
+               int count = 1024;
+               // int keyLength = 256;
+               int keyLength = 128;
+
+               String cipherAlgorithm = "AES/CBC/PKCS5Padding";
+               String secretKeyAlgorithm = "PBKDF2WithHmacSHA1";
+               SecretKeyFactory keyFac = SecretKeyFactory
+                               .getInstance(secretKeyAlgorithm);
+               PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), salt,
+                               count, keyLength);
+               SecretKey tmp = keyFac.generateSecret(pbeKeySpec);
+               SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");
+               Cipher ecipher = Cipher.getInstance(cipherAlgorithm);
+               ecipher.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(iv));
+
+               // decrypt
+               keyFac = SecretKeyFactory.getInstance(secretKeyAlgorithm);
+               pbeKeySpec = new PBEKeySpec(password.toCharArray(), salt, count,
+                               keyLength);
+               tmp = keyFac.generateSecret(pbeKeySpec);
+               secret = new SecretKeySpec(tmp.getEncoded(), "AES");
+               // AlgorithmParameters params = ecipher.getParameters();
+               // byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
+               Cipher dcipher = Cipher.getInstance(cipherAlgorithm);
+               dcipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
+
+               byte[] encrypted = ecipher.doFinal(message.getBytes());
+               byte[] decrypted = dcipher.doFinal(encrypted);
+               assertEquals(message, new String(decrypted));
+
+               ByteArrayOutputStream out = new ByteArrayOutputStream();
+               CipherOutputStream cipherOut = new CipherOutputStream(out, ecipher);
+               cipherOut.write(message.getBytes());
+               IOUtils.closeQuietly(cipherOut);
+               byte[] enc = out.toByteArray();
+
+               ByteArrayInputStream in = new ByteArrayInputStream(enc);
+               CipherInputStream cipherIn = new CipherInputStream(in, dcipher);
+               ByteArrayOutputStream dec = new ByteArrayOutputStream();
+               IOUtils.copy(cipherIn, dec);
+               assertEquals(message, new String(dec.toByteArray()));
+       }
+}
diff --git a/org.argeo.cms/ext/test/org/argeo/cms/security/RunHttpSpnego.java b/org.argeo.cms/ext/test/org/argeo/cms/security/RunHttpSpnego.java
new file mode 100644 (file)
index 0000000..f090ac4
--- /dev/null
@@ -0,0 +1,32 @@
+package org.argeo.cms.security;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+import java.net.URL;
+
+public class RunHttpSpnego {
+
+    static final String kuser = "mbaudier@ARGEO.EU"; // your account name
+    static final String kpass = "test"; // retrieve password for your account 
+
+    static class MyAuthenticator extends Authenticator {
+        public PasswordAuthentication getPasswordAuthentication() {
+            // I haven't checked getRequestingScheme() here, since for NTLM
+            // and Negotiate, the usrname and password are all the same.
+            System.err.println("Feeding username and password for " + getRequestingScheme());
+            return (new PasswordAuthentication(kuser, kpass.toCharArray()));
+        }
+    }
+
+    public static void main(String[] args) throws Exception {
+        Authenticator.setDefault(new MyAuthenticator());
+        URL url = new URL(args[0]);
+        InputStream ins = url.openConnection().getInputStream();
+        BufferedReader reader = new BufferedReader(new InputStreamReader(ins));
+        String str;
+        while((str = reader.readLine()) != null)
+            System.out.println(str);
+    }
+}
diff --git a/org.argeo.cms/ext/test/org/argeo/cms/tabular/JcrTabularTest.java b/org.argeo.cms/ext/test/org/argeo/cms/tabular/JcrTabularTest.java
new file mode 100644 (file)
index 0000000..112562c
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.tabular;
+
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.jackrabbit.unit.AbstractJackrabbitTestCase;
+import org.argeo.node.tabular.TabularColumn;
+import org.argeo.node.tabular.TabularRow;
+import org.argeo.node.tabular.TabularRowIterator;
+import org.argeo.node.tabular.TabularWriter;
+
+public class JcrTabularTest extends AbstractJackrabbitTestCase {
+       private final static Log log = LogFactory.getLog(JcrTabularTest.class);
+
+       public void testWriteReadCsv() throws Exception {
+               session().setNamespacePrefix("argeo", ArgeoNames.ARGEO_NAMESPACE);
+               InputStreamReader reader = new InputStreamReader(getClass()
+                               .getResourceAsStream("/org/argeo/node/node.cnd"));
+               CndImporter.registerNodeTypes(reader, session());
+               reader.close();
+               reader = new InputStreamReader(getClass()
+                               .getResourceAsStream("/org/argeo/cms/cms.cnd"));
+               CndImporter.registerNodeTypes(reader, session());
+               reader.close();
+
+               // write
+               Integer columnCount = 15;
+               Long rowCount = 1000l;
+               String stringValue = "test, \ntest";
+
+               List<TabularColumn> header = new ArrayList<TabularColumn>();
+               for (int i = 0; i < columnCount; i++) {
+                       header.add(new TabularColumn("col" + i, PropertyType.STRING));
+               }
+               Node tableNode = session().getRootNode().addNode("table",
+                               ArgeoTypes.ARGEO_TABLE);
+               TabularWriter writer = new JcrTabularWriter(tableNode, header,
+                               ArgeoTypes.ARGEO_CSV);
+               for (int i = 0; i < rowCount; i++) {
+                       List<Object> objs = new ArrayList<Object>();
+                       for (int j = 0; j < columnCount; j++) {
+                               objs.add(stringValue);
+                       }
+                       writer.appendRow(objs.toArray());
+               }
+               writer.close();
+               session().save();
+
+               if (log.isDebugEnabled())
+                       log.debug("Wrote tabular content " + rowCount + " rows, "
+                                       + columnCount + " columns");
+               // read
+               TabularRowIterator rowIt = new JcrTabularRowIterator(tableNode);
+               Long count = 0l;
+               while (rowIt.hasNext()) {
+                       TabularRow tr = rowIt.next();
+                       assertEquals(header.size(), tr.size());
+                       count++;
+               }
+               assertEquals(rowCount, count);
+               if (log.isDebugEnabled())
+                       log.debug("Read tabular content " + rowCount + " rows, "
+                                       + columnCount + " columns");
+       }
+}
diff --git a/org.argeo.cms/pom.xml b/org.argeo.cms/pom.xml
new file mode 100644 (file)
index 0000000..b64ff10
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.cms</artifactId>
+       <name>CMS</name>
+       <packaging>jar</packaging>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.node.api</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.jcr</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.enterprise</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <scope>provided</scope>
+               </dependency>
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/ArgeoNames.java b/org.argeo.cms/src/org/argeo/cms/ArgeoNames.java
new file mode 100644 (file)
index 0000000..c2c6713
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms;
+
+/** JCR names in the http://www.argeo.org/argeo namespace */
+public interface ArgeoNames {
+       public final static String ARGEO_NAMESPACE = "http://www.argeo.org/ns/argeo";
+//     public final static String ARGEO = "argeo";
+
+       public final static String ARGEO_URI = "argeo:uri";
+       public final static String ARGEO_USER_ID = "argeo:userID";
+//     public final static String ARGEO_PREFERENCES = "argeo:preferences";
+//     public final static String ARGEO_DATA_MODEL_VERSION = "argeo:dataModelVersion";
+
+       public final static String ARGEO_REMOTE = "argeo:remote";
+       public final static String ARGEO_PASSWORD = "argeo:password";
+//     public final static String ARGEO_REMOTE_ROLES = "argeo:remoteRoles";
+
+       // user profile
+//     public final static String ARGEO_PROFILE = "argeo:profile";
+
+       // spring security
+//     @Deprecated
+//     public final static String ARGEO_ENABLED = "argeo:enabled";
+//
+//     // personal details
+//     /** @deprecated Use org.argeo.naming.LdapAttrs */
+//     @Deprecated
+//     public final static String ARGEO_FIRST_NAME = "argeo:firstName";
+//     /** @deprecated Use org.argeo.naming.LdapAttrs */
+//     @Deprecated
+//     public final static String ARGEO_LAST_NAME = "argeo:lastName";
+//     /** @deprecated Use org.argeo.naming.LdapAttrs */
+//     @Deprecated
+//     public final static String ARGEO_PRIMARY_EMAIL = "argeo:primaryEmail";
+//     /** @deprecated Use org.argeo.naming.LdapAttrs */
+//     @Deprecated
+//     public final static String ARGEO_PRIMARY_ORGANIZATION = "argeo:primaryOrganization";
+
+       // tabular
+       public final static String ARGEO_IS_KEY = "argeo:isKey";
+
+       // crypto
+       public final static String ARGEO_IV = "argeo:iv";
+       public final static String ARGEO_SECRET_KEY_FACTORY = "argeo:secretKeyFactory";
+       public final static String ARGEO_SALT = "argeo:salt";
+       public final static String ARGEO_ITERATION_COUNT = "argeo:iterationCount";
+       public final static String ARGEO_KEY_LENGTH = "argeo:keyLength";
+       public final static String ARGEO_SECRET_KEY_ENCRYPTION = "argeo:secretKeyEncryption";
+       public final static String ARGEO_CIPHER = "argeo:cipher";
+       public final static String ARGEO_KEYRING = "argeo:keyring";
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/ArgeoTypes.java b/org.argeo.cms/src/org/argeo/cms/ArgeoTypes.java
new file mode 100644 (file)
index 0000000..9f4cd0e
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms;
+
+/** JCR types in the http://www.argeo.org/argeo namespace */
+public interface ArgeoTypes {
+//     public final static String ARGEO_LINK = "argeo:link";
+//     public final static String ARGEO_USER_HOME = "argeo:userHome";
+//     public final static String ARGEO_USER_PROFILE = "argeo:userProfile";
+       public final static String ARGEO_REMOTE_REPOSITORY = "argeo:remoteRepository";
+//     public final static String ARGEO_PREFERENCE_NODE = "argeo:preferenceNode";
+
+       // data model
+//     public final static String ARGEO_DATA_MODEL = "argeo:dataModel";
+       
+       // tabular
+       public final static String ARGEO_TABLE = "argeo:table";
+       public final static String ARGEO_COLUMN = "argeo:column";
+       public final static String ARGEO_CSV = "argeo:csv";
+
+       // crypto
+       public final static String ARGEO_ENCRYPTED = "argeo:encrypted";
+       public final static String ARGEO_PBE_SPEC = "argeo:pbeSpec";
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/CmsException.java b/org.argeo.cms/src/org/argeo/cms/CmsException.java
new file mode 100644 (file)
index 0000000..fb11c89
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.cms;
+
+/** CMS specific exceptions. */
+public class CmsException extends RuntimeException {
+       private static final long serialVersionUID = -5341764743356771313L;
+
+       public CmsException(String message) {
+               super(message);
+       }
+
+       public CmsException(String message, Throwable e) {
+               super(message, e);
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/CmsMsg.java b/org.argeo.cms/src/org/argeo/cms/CmsMsg.java
new file mode 100644 (file)
index 0000000..718bfa4
--- /dev/null
@@ -0,0 +1,13 @@
+package org.argeo.cms;
+
+import org.argeo.cms.i18n.Localized;
+
+public enum CmsMsg implements Localized {
+       username, password, login, logout, register,
+       // password
+       changePassword, currentPassword, newPassword, repeatNewPassword, passwordChanged,
+       // dialog
+       close, cancel, ok,
+       // wizard
+       wizardBack, wizardNext, wizardFinish;
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/CmsNames.java b/org.argeo.cms/src/org/argeo/cms/CmsNames.java
new file mode 100644 (file)
index 0000000..be10b76
--- /dev/null
@@ -0,0 +1,24 @@
+package org.argeo.cms;
+
+/** JCR names. */
+public interface CmsNames {
+       /*
+        * TEXT
+        */
+       public final static String CMS_DRAFTS = "cms:drafts";
+
+       public final static String CMS_P = "cms:p";
+       public final static String CMS_H = "cms:h";
+
+       public final static String CMS_CONTENT = "cms:content";
+       public final static String CMS_STYLE = "cms:style";
+
+       public final static String CMS_INDEX = "cms:index";
+
+       /*
+        * IMAGES
+        */
+       public final static String CMS_IMAGE_WIDTH = "cms:imageWidth";
+       public final static String CMS_IMAGE_HEIGHT = "cms:imageHeight";
+       public final static String CMS_DATA = "cms:data";
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/CmsTypes.java b/org.argeo.cms/src/org/argeo/cms/CmsTypes.java
new file mode 100644 (file)
index 0000000..a8ef076
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.cms;
+
+/** JCR types. */
+public interface CmsTypes {
+       public final static String CMS_TEXT = "cms:text";
+       public final static String CMS_IMAGE = "cms:image";
+       public final static String CMS_SECTION = "cms:section";
+       public final static String CMS_STYLED = "cms:styled";
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/AnonymousLoginModule.java
new file mode 100644 (file)
index 0000000..19c0d60
--- /dev/null
@@ -0,0 +1,77 @@
+package org.argeo.cms.auth;
+
+import java.util.Locale;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.UserAdmin;
+
+/** Anonymous CMS user */
+public class AnonymousLoginModule implements LoginModule {
+       private final static Log log = LogFactory.getLog(AnonymousLoginModule.class);
+
+       private Subject subject;
+       private Map<String, Object> sharedState = null;
+
+       // private state
+       private BundleContext bc;
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+                       Map<String, ?> options) {
+               this.subject = subject;
+               this.sharedState = (Map<String, Object>) sharedState;
+               try {
+                       bc = FrameworkUtil.getBundle(AnonymousLoginModule.class).getBundleContext();
+                       assert bc != null;
+               } catch (Exception e) {
+                       throw new CmsException("Cannot initialize login module", e);
+               }
+       }
+
+       @Override
+       public boolean login() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean commit() throws LoginException {
+               UserAdmin userAdmin = bc.getService(bc.getServiceReference(UserAdmin.class));
+               Authorization authorization = userAdmin.getAuthorization(null);
+               HttpServletRequest request = (HttpServletRequest) sharedState.get(CmsAuthUtils.SHARED_STATE_HTTP_REQUEST);
+               Locale locale = Locale.getDefault();
+               if (request != null)
+                       locale = request.getLocale();
+               CmsAuthUtils.addAuthorization(subject, authorization, locale, request);
+               CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale);
+               if (log.isTraceEnabled())
+                       log.trace("Anonymous logged in to CMS: " + subject);
+               return true;
+       }
+
+       @Override
+       public boolean abort() throws LoginException {
+               // authorization = null;
+               return true;
+       }
+
+       @Override
+       public boolean logout() throws LoginException {
+               if (log.isTraceEnabled())
+                       log.trace("Logging out anonymous from CMS... " + subject);
+               CmsAuthUtils.cleanUp(subject);
+               return true;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java
new file mode 100644 (file)
index 0000000..dde2d73
--- /dev/null
@@ -0,0 +1,191 @@
+package org.argeo.cms.auth;
+
+import java.security.Principal;
+import java.util.Locale;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+//import org.apache.jackrabbit.core.security.AnonymousPrincipal;
+//import org.apache.jackrabbit.core.security.SecurityConstants;
+//import org.apache.jackrabbit.core.security.principal.AdminPrincipal;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.internal.auth.CmsSessionImpl;
+import org.argeo.cms.internal.auth.ImpliedByPrincipal;
+import org.argeo.cms.internal.http.WebCmsSessionImpl;
+import org.argeo.cms.internal.kernel.Activator;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.security.AnonymousPrincipal;
+import org.argeo.node.security.DataAdminPrincipal;
+import org.argeo.node.security.NodeSecurityUtils;
+import org.argeo.osgi.useradmin.AuthenticatingUser;
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.useradmin.Authorization;
+
+class CmsAuthUtils {
+       // Standard
+       final static String SHARED_STATE_NAME = AuthenticatingUser.SHARED_STATE_NAME;
+       final static String SHARED_STATE_PWD = AuthenticatingUser.SHARED_STATE_PWD;
+       final static String HEADER_AUTHORIZATION = "Authorization";
+       final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
+
+       // Argeo specific
+       final static String SHARED_STATE_HTTP_REQUEST = "org.argeo.cms.auth.http.request";
+       final static String SHARED_STATE_SPNEGO_TOKEN = "org.argeo.cms.auth.spnegoToken";
+       final static String SHARED_STATE_SPNEGO_OUT_TOKEN = "org.argeo.cms.auth.spnegoOutToken";
+       final static String SHARED_STATE_CERTIFICATE_CHAIN = "org.argeo.cms.auth.certificateChain";
+
+       static void addAuthorization(Subject subject, Authorization authorization, Locale locale,
+                       HttpServletRequest request) {
+               assert subject != null;
+               checkSubjectEmpty(subject);
+               assert authorization != null;
+
+               // required for display name:
+               subject.getPrivateCredentials().add(authorization);
+
+               if (Activator.isSingleUser()) {
+                       subject.getPrincipals().add(new DataAdminPrincipal());
+               }
+
+               Set<Principal> principals = subject.getPrincipals();
+               try {
+                       String authName = authorization.getName();
+
+                       // determine user's principal
+                       final LdapName name;
+                       final Principal userPrincipal;
+                       if (authName == null) {
+                               name = NodeSecurityUtils.ROLE_ANONYMOUS_NAME;
+                               userPrincipal = new AnonymousPrincipal();
+                               principals.add(userPrincipal);
+                       } else {
+                               name = new LdapName(authName);
+                               NodeSecurityUtils.checkUserName(name);
+                               userPrincipal = new X500Principal(name.toString());
+                               principals.add(userPrincipal);
+                               // principals.add(new ImpliedByPrincipal(NodeSecurityUtils.ROLE_USER_NAME,
+                               // userPrincipal));
+                       }
+
+                       // Add roles provided by authorization
+                       for (String role : authorization.getRoles()) {
+                               LdapName roleName = new LdapName(role);
+                               if (roleName.equals(name)) {
+                                       // skip
+                               } else if (roleName.equals(NodeSecurityUtils.ROLE_ANONYMOUS_NAME)) {
+                                       // skip
+                               } else {
+                                       NodeSecurityUtils.checkImpliedPrincipalName(roleName);
+                                       principals.add(new ImpliedByPrincipal(roleName.toString(), userPrincipal));
+                                       if (roleName.equals(NodeSecurityUtils.ROLE_ADMIN_NAME))
+                                               principals.add(new DataAdminPrincipal());
+                               }
+                       }
+
+               } catch (InvalidNameException e) {
+                       throw new CmsException("Cannot commit", e);
+               }
+
+               // registerSessionAuthorization(request, subject, authorization, locale);
+       }
+
+       private static void checkSubjectEmpty(Subject subject) {
+               if (!subject.getPrincipals(AnonymousPrincipal.class).isEmpty())
+                       throw new IllegalStateException("Already logged in as anonymous: " + subject);
+               if (!subject.getPrincipals(X500Principal.class).isEmpty())
+                       throw new IllegalStateException("Already logged in as user: " + subject);
+               if (!subject.getPrincipals(DataAdminPrincipal.class).isEmpty())
+                       throw new IllegalStateException("Already logged in as data admin: " + subject);
+               if (!subject.getPrincipals(ImpliedByPrincipal.class).isEmpty())
+                       throw new IllegalStateException("Already authorized: " + subject);
+       }
+
+       static void cleanUp(Subject subject) {
+               // Argeo
+               subject.getPrincipals().removeAll(subject.getPrincipals(X500Principal.class));
+               subject.getPrincipals().removeAll(subject.getPrincipals(ImpliedByPrincipal.class));
+               subject.getPrincipals().removeAll(subject.getPrincipals(AnonymousPrincipal.class));
+               subject.getPrincipals().removeAll(subject.getPrincipals(DataAdminPrincipal.class));
+
+               subject.getPrivateCredentials().removeAll(subject.getPrivateCredentials(CmsSessionId.class));
+               subject.getPrivateCredentials().removeAll(subject.getPrivateCredentials(Authorization.class));
+               // Jackrabbit
+               // subject.getPrincipals().removeAll(subject.getPrincipals(AdminPrincipal.class));
+               // subject.getPrincipals().removeAll(subject.getPrincipals(AnonymousPrincipal.class));
+       }
+
+       synchronized static void registerSessionAuthorization(HttpServletRequest request, Subject subject,
+                       Authorization authorization, Locale locale) {
+               // synchronized in order to avoid multiple registrations
+               // TODO move it to a service in order to avoid static synchronization
+               if (request != null) {
+                       HttpSession httpSession = request.getSession(false);
+                       assert httpSession != null;
+                       String httpSessId = httpSession.getId();
+                       String remoteUser = authorization.getName() != null ? authorization.getName()
+                                       : NodeConstants.ROLE_ANONYMOUS;
+                       request.setAttribute(HttpContext.REMOTE_USER, remoteUser);
+                       request.setAttribute(HttpContext.AUTHORIZATION, authorization);
+
+                       CmsSessionImpl cmsSession = CmsSessionImpl.getByLocalId(httpSessId);
+                       if (cmsSession != null) {
+                               if (authorization.getName() != null) {
+                                       if (cmsSession.getAuthorization().getName() == null) {
+                                               cmsSession.close();
+                                               cmsSession = null;
+                                       } else if (!authorization.getName().equals(cmsSession.getAuthorization().getName())) {
+                                               throw new CmsException("Inconsistent user " + authorization.getName()
+                                                               + " for existing CMS session " + cmsSession);
+                                       }
+                                       // keyring
+                                       if (cmsSession != null)
+                                               subject.getPrivateCredentials().addAll(cmsSession.getSecretKeys());
+                               } else {// anonymous
+                                       if (cmsSession.getAuthorization().getName() != null) {
+                                               cmsSession.close();
+                                               // TODO rather throw an exception ? log a warning ?
+                                               cmsSession = null;
+                                       }
+                               }
+                       } else if (cmsSession == null) {
+                               cmsSession = new WebCmsSessionImpl(subject, authorization, locale, request);
+                       }
+                       // request.setAttribute(CmsSession.class.getName(), cmsSession);
+                       if (cmsSession != null) {
+                               CmsSessionId nodeSessionId = new CmsSessionId(cmsSession.getUuid());
+                               if (subject.getPrivateCredentials(CmsSessionId.class).size() == 0)
+                                       subject.getPrivateCredentials().add(nodeSessionId);
+                               else {
+                                       UUID storedSessionId = subject.getPrivateCredentials(CmsSessionId.class).iterator().next()
+                                                       .getUuid();
+                                       // if (storedSessionId.equals(httpSessionId.getValue()))
+                                       throw new CmsException(
+                                                       "Subject already logged with session " + storedSessionId + " (not " + nodeSessionId + ")");
+                               }
+                       }
+               } else {
+                       // TODO desktop, CLI
+               }
+       }
+
+       public static <T extends Principal> T getSinglePrincipal(Subject subject, Class<T> clss) {
+               Set<T> principals = subject.getPrincipals(clss);
+               if (principals.isEmpty())
+                       return null;
+               if (principals.size() > 1)
+                       throw new IllegalStateException("Only one " + clss + " principal expected in " + subject);
+               return principals.iterator().next();
+       }
+
+       private CmsAuthUtils() {
+
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsSession.java
new file mode 100644 (file)
index 0000000..f6984bc
--- /dev/null
@@ -0,0 +1,44 @@
+package org.argeo.cms.auth;
+
+import java.time.ZonedDateTime;
+import java.util.Locale;
+import java.util.UUID;
+
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.LdapAttrs;
+import org.osgi.service.useradmin.Authorization;
+
+public interface CmsSession {
+       final static String USER_DN = LdapAttrs.DN;
+       final static String SESSION_UUID = LdapAttrs.entryUUID.name();
+       final static String SESSION_LOCAL_ID = LdapAttrs.uniqueIdentifier.name();
+
+       // public String getId();
+
+       UUID getUuid();
+
+       LdapName getUserDn();
+
+       String getLocalId();
+
+       Authorization getAuthorization();
+
+       boolean isAnonymous();
+
+       ZonedDateTime getCreationTime();
+       ZonedDateTime getEnd();
+       
+       Locale getLocale();
+
+       boolean isValid();
+
+       // public Session getDataSession(String cn, String workspace, Repository
+       // repository);
+       //
+       // public void releaseDataSession(String cn, Session session);
+
+       // public void addHttpSession(HttpServletRequest request);
+
+       // public void cleanUp();
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsSessionId.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsSessionId.java
new file mode 100644 (file)
index 0000000..8753289
--- /dev/null
@@ -0,0 +1,35 @@
+package org.argeo.cms.auth;
+
+import java.util.UUID;
+
+import org.argeo.cms.CmsException;
+
+public class CmsSessionId {
+       private final UUID uuid;
+
+       public CmsSessionId(UUID value) {
+               if (value == null)
+                       throw new CmsException("value cannot be null");
+               this.uuid = value;
+       }
+
+       public UUID getUuid() {
+               return uuid;
+       }
+
+       @Override
+       public int hashCode() {
+               return uuid.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               return obj instanceof CmsSessionId && ((CmsSessionId) obj).getUuid().equals(uuid);
+       }
+
+       @Override
+       public String toString() {
+               return "Node Session " + uuid;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java b/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java
new file mode 100644 (file)
index 0000000..34f4457
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.auth;
+
+import java.security.AccessController;
+import java.security.Principal;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.security.acl.Group;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.internal.auth.CmsSessionImpl;
+import org.argeo.cms.internal.kernel.Activator;
+import org.argeo.node.NodeConstants;
+import org.osgi.service.useradmin.Authorization;
+
+/**
+ * Programmatic access to the currently authenticated user, within a CMS
+ * context.
+ */
+public final class CurrentUser {
+       // private final static Log log = LogFactory.getLog(CurrentUser.class);
+       // private final static BundleContext bc =
+       // FrameworkUtil.getBundle(CurrentUser.class).getBundleContext();
+       /*
+        * CURRENT USER API
+        */
+
+       /**
+        * Technical username of the currently authenticated user.
+        * 
+        * @return the authenticated username or null if not authenticated / anonymous
+        */
+       public static String getUsername() {
+               return getUsername(currentSubject());
+       }
+
+       /**
+        * Human readable name of the currently authenticated user (typically first name
+        * and last name).
+        */
+       public static String getDisplayName() {
+               return getDisplayName(currentSubject());
+       }
+
+       /** Whether a user is currently authenticated. */
+       public static boolean isAnonymous() {
+               return isAnonymous(currentSubject());
+       }
+
+       /** Locale of the current user */
+       public final static Locale locale() {
+               return locale(currentSubject());
+       }
+
+       /** Roles of the currently logged-in user */
+       public final static Set<String> roles() {
+               return roles(currentSubject());
+       }
+
+       /** Returns true if the current user is in the specified role */
+       public static boolean isInRole(String role) {
+               Set<String> roles = roles();
+               return roles.contains(role);
+       }
+
+       /** Executes as the current user */
+       public final static <T> T doAs(PrivilegedAction<T> action) {
+               return Subject.doAs(currentSubject(), action);
+       }
+
+       /** Executes as the current user */
+       public final static <T> T tryAs(PrivilegedExceptionAction<T> action) throws PrivilegedActionException {
+               return Subject.doAs(currentSubject(), action);
+       }
+
+       /*
+        * WRAPPERS
+        */
+
+       public final static String getUsername(Subject subject) {
+               if (subject == null)
+                       throw new CmsException("Subject cannot be null");
+               if (subject.getPrincipals(X500Principal.class).size() != 1)
+                       return NodeConstants.ROLE_ANONYMOUS;
+               Principal principal = subject.getPrincipals(X500Principal.class).iterator().next();
+               return principal.getName();
+       }
+
+       public final static String getDisplayName(Subject subject) {
+               return getAuthorization(subject).toString();
+       }
+
+       public final static Set<String> roles(Subject subject) {
+               Set<String> roles = new HashSet<String>();
+               roles.add(getUsername(subject));
+               for (Principal group : subject.getPrincipals(Group.class)) {
+                       roles.add(group.getName());
+               }
+               return roles;
+       }
+
+       public final static Locale locale(Subject subject) {
+               Set<Locale> locales = subject.getPublicCredentials(Locale.class);
+               if (locales.isEmpty()) {
+                       Locale defaultLocale = Activator.getNodeState().getDefaultLocale();
+                       return defaultLocale;
+               } else
+                       return locales.iterator().next();
+       }
+
+       /** Whether this user is currently authenticated. */
+       public static boolean isAnonymous(Subject subject) {
+               if (subject == null)
+                       return true;
+               String username = getUsername(subject);
+               return username == null || username.equalsIgnoreCase(NodeConstants.ROLE_ANONYMOUS);
+       }
+
+       public CmsSession getCmsSession() {
+               Subject subject = currentSubject();
+               CmsSessionId cmsSessionId = subject.getPrivateCredentials(CmsSessionId.class).iterator().next();
+               return CmsSessionImpl.getByUuid(cmsSessionId.getUuid());
+       }
+
+       /*
+        * HELPERS
+        */
+       private static Subject currentSubject() {
+               // CmsAuthenticated cmsView = getNodeAuthenticated();
+               // if (cmsView != null)
+               // return cmsView.getSubject();
+               Subject subject = getAccessControllerSubject();
+               if (subject != null)
+                       return subject;
+               throw new CmsException("Cannot find related subject");
+       }
+
+       private static Subject getAccessControllerSubject() {
+               return Subject.getSubject(AccessController.getContext());
+       }
+
+       // public static boolean isAuthenticated() {
+       // return getAccessControllerSubject() != null;
+       // }
+
+       /**
+        * The node authenticated component (typically a CMS view) related to this
+        * display, or null if none is available from this call. <b>Not API: Only for
+        * low-level access.</b>
+        */
+       // private static CmsAuthenticated getNodeAuthenticated() {
+       // return UiContext.getData(CmsAuthenticated.KEY);
+       // }
+
+       private static Authorization getAuthorization(Subject subject) {
+               return subject.getPrivateCredentials(Authorization.class).iterator().next();
+       }
+
+       public static boolean logoutCmsSession(Subject subject) {
+               UUID nodeSessionId;
+               if (subject.getPrivateCredentials(CmsSessionId.class).size() == 1)
+                       nodeSessionId = subject.getPrivateCredentials(CmsSessionId.class).iterator().next().getUuid();
+               else
+                       return false;
+               CmsSessionImpl cmsSession = CmsSessionImpl.getByUuid(nodeSessionId.toString());
+               cmsSession.close();
+               // if (log.isDebugEnabled())
+               // log.debug("Logged out CMS session " + cmsSession.getUuid());
+               return true;
+       }
+
+       private CurrentUser() {
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/DataAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/DataAdminLoginModule.java
new file mode 100644 (file)
index 0000000..50a8788
--- /dev/null
@@ -0,0 +1,44 @@
+package org.argeo.cms.auth;
+
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+import org.argeo.node.security.DataAdminPrincipal;
+
+/** Logs a system process as data admin */
+public class DataAdminLoginModule implements LoginModule {
+       private Subject subject;
+
+       @Override
+       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+                       Map<String, ?> options) {
+               this.subject = subject;
+       }
+
+       @Override
+       public boolean login() throws LoginException {
+               // TODO check permission?
+               return true;
+       }
+
+       @Override
+       public boolean commit() throws LoginException {
+               subject.getPrincipals().add(new DataAdminPrincipal());
+               return true;
+       }
+
+       @Override
+       public boolean abort() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean logout() throws LoginException {
+               subject.getPrincipals().removeAll(subject.getPrincipals(DataAdminPrincipal.class));
+               return true;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/HttpRequestCallback.java b/org.argeo.cms/src/org/argeo/cms/auth/HttpRequestCallback.java
new file mode 100644 (file)
index 0000000..611b324
--- /dev/null
@@ -0,0 +1,27 @@
+package org.argeo.cms.auth;
+
+import javax.security.auth.callback.Callback;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class HttpRequestCallback implements Callback {
+       private HttpServletRequest request;
+       private HttpServletResponse response;
+
+       public HttpServletRequest getRequest() {
+               return request;
+       }
+
+       public void setRequest(HttpServletRequest request) {
+               this.request = request;
+       }
+
+       public HttpServletResponse getResponse() {
+               return response;
+       }
+
+       public void setResponse(HttpServletResponse response) {
+               this.response = response;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/HttpRequestCallbackHandler.java b/org.argeo.cms/src/org/argeo/cms/auth/HttpRequestCallbackHandler.java
new file mode 100644 (file)
index 0000000..bcc403f
--- /dev/null
@@ -0,0 +1,36 @@
+package org.argeo.cms.auth;
+
+import java.io.IOException;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.LanguageCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Callback handler populating {@link HttpRequestCallback}s with the provided
+ * {@link HttpServletRequest}, and ignoring any other callback.
+ */
+public class HttpRequestCallbackHandler implements CallbackHandler {
+       final private HttpServletRequest request;
+       final private HttpServletResponse response;
+
+       public HttpRequestCallbackHandler(HttpServletRequest request, HttpServletResponse response) {
+               this.request = request;
+               this.response = response;
+       }
+
+       @Override
+       public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+               for (Callback callback : callbacks)
+                       if (callback instanceof HttpRequestCallback) {
+                               ((HttpRequestCallback) callback).setRequest(request);
+                               ((HttpRequestCallback) callback).setResponse(response);
+                       } else if (callback instanceof LanguageCallback) {
+                               ((LanguageCallback) callback).setLocale(request.getLocale());
+                       }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java
new file mode 100644 (file)
index 0000000..cbd5406
--- /dev/null
@@ -0,0 +1,223 @@
+package org.argeo.cms.auth;
+
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringTokenizer;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.internal.kernel.Activator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.http.HttpContext;
+import org.osgi.service.useradmin.Authorization;
+
+public class HttpSessionLoginModule implements LoginModule {
+       private final static Log log = LogFactory.getLog(HttpSessionLoginModule.class);
+
+       private Subject subject = null;
+       private CallbackHandler callbackHandler = null;
+       private Map<String, Object> sharedState = null;
+
+       private HttpServletRequest request = null;
+       private HttpServletResponse response = null;
+
+       private BundleContext bc;
+
+       private Authorization authorization;
+       private Locale locale;
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+                       Map<String, ?> options) {
+               bc = FrameworkUtil.getBundle(HttpSessionLoginModule.class).getBundleContext();
+               assert bc != null;
+               this.subject = subject;
+               this.callbackHandler = callbackHandler;
+               this.sharedState = (Map<String, Object>) sharedState;
+       }
+
+       @Override
+       public boolean login() throws LoginException {
+               if (callbackHandler == null)
+                       return false;
+               HttpRequestCallback httpCallback = new HttpRequestCallback();
+               try {
+                       callbackHandler.handle(new Callback[] { httpCallback });
+               } catch (IOException e) {
+                       throw new LoginException("Cannot handle http callback: " + e.getMessage());
+               } catch (UnsupportedCallbackException e) {
+                       return false;
+               }
+               request = httpCallback.getRequest();
+               if (request == null)
+                       return false;
+               authorization = (Authorization) request.getAttribute(HttpContext.AUTHORIZATION);
+               if (authorization == null) {// search by session ID
+                       HttpSession httpSession = request.getSession(false);
+                       if (httpSession == null) {
+                               // TODO make sure this is always safe
+                               if (log.isTraceEnabled())
+                                       log.trace("Create http session");
+                               httpSession = request.getSession(true);
+                       }
+                       String httpSessionId = httpSession.getId();
+                       // authorization = (Authorization)
+                       // request.getSession().getAttribute(HttpContext.AUTHORIZATION);
+                       // if (authorization == null) {
+                       Collection<ServiceReference<CmsSession>> sr;
+                       try {
+                               sr = bc.getServiceReferences(CmsSession.class,
+                                               "(" + CmsSession.SESSION_LOCAL_ID + "=" + httpSessionId + ")");
+                       } catch (InvalidSyntaxException e) {
+                               throw new CmsException("Cannot get CMS session for id " + httpSessionId, e);
+                       }
+                       if (sr.size() == 1) {
+                               CmsSession cmsSession = bc.getService(sr.iterator().next());
+                               locale = cmsSession.getLocale();
+                               authorization = cmsSession.getAuthorization();
+                               if (authorization.getName() == null)
+                                       authorization = null;// anonymous is not sufficient
+                               if (log.isTraceEnabled())
+                                       log.trace("Retrieved authorization from " + cmsSession);
+                       } else if (sr.size() == 0)
+                               authorization = null;
+                       else
+                               throw new CmsException(sr.size() + ">1 web sessions detected for http session " + httpSessionId);
+
+               }
+               sharedState.put(CmsAuthUtils.SHARED_STATE_HTTP_REQUEST, request);
+               extractHttpAuth(request);
+               extractClientCertificate(request);
+               if (authorization == null) {
+                       return false;
+               } else {
+                       return true;
+               }
+       }
+
+       @Override
+       public boolean commit() throws LoginException {
+               byte[] outToken = (byte[]) sharedState.get(CmsAuthUtils.SHARED_STATE_SPNEGO_OUT_TOKEN);
+               if (outToken != null) {
+                       response.setHeader(CmsAuthUtils.HEADER_WWW_AUTHENTICATE,
+                                       "Negotiate " + java.util.Base64.getEncoder().encodeToString(outToken));
+               }
+
+               if (authorization != null) {
+                       // Locale locale = request.getLocale();
+                       if (locale == null)
+                               locale = request.getLocale();
+                       subject.getPublicCredentials().add(locale);
+                       CmsAuthUtils.addAuthorization(subject, authorization, locale, request);
+                       CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale);
+                       cleanUp();
+                       return true;
+               } else {
+                       cleanUp();
+                       return false;
+               }
+       }
+
+       @Override
+       public boolean abort() throws LoginException {
+               cleanUp();
+               return false;
+       }
+
+       private void cleanUp() {
+               authorization = null;
+               request = null;
+       }
+
+       @Override
+       public boolean logout() throws LoginException {
+               cleanUp();
+               return true;
+       }
+
+       private void extractHttpAuth(final HttpServletRequest httpRequest) {
+               String authHeader = httpRequest.getHeader(CmsAuthUtils.HEADER_AUTHORIZATION);
+               if (authHeader != null) {
+                       StringTokenizer st = new StringTokenizer(authHeader);
+                       if (st.hasMoreTokens()) {
+                               String basic = st.nextToken();
+                               if (basic.equalsIgnoreCase("Basic")) {
+                                       try {
+                                               // TODO manipulate char[]
+                                               Base64.Decoder decoder = Base64.getDecoder();
+                                               String credentials = new String(decoder.decode(st.nextToken()), "UTF-8");
+                                               // log.debug("Credentials: " + credentials);
+                                               int p = credentials.indexOf(":");
+                                               if (p != -1) {
+                                                       final String login = credentials.substring(0, p).trim();
+                                                       final char[] password = credentials.substring(p + 1).trim().toCharArray();
+                                                       sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, login);
+                                                       sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, password);
+                                               } else {
+                                                       throw new CmsException("Invalid authentication token");
+                                               }
+                                       } catch (Exception e) {
+                                               throw new CmsException("Couldn't retrieve authentication", e);
+                                       }
+                               } else if (basic.equalsIgnoreCase("Negotiate")) {
+                                       String spnegoToken = st.nextToken();
+                                       Base64.Decoder decoder = Base64.getDecoder();
+                                       byte[] authToken = decoder.decode(spnegoToken);
+                                       sharedState.put(CmsAuthUtils.SHARED_STATE_SPNEGO_TOKEN, authToken);
+                               }
+                       }
+               }
+
+               // auth token
+               // String mail = request.getParameter(LdapAttrs.mail.name());
+               // String authPassword = request.getParameter(LdapAttrs.authPassword.name());
+               // if (authPassword != null) {
+               // sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, authPassword);
+               // if (mail != null)
+               // sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, mail);
+               // }
+       }
+
+       private void extractClientCertificate(HttpServletRequest req) {
+               X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
+               if (null != certs && certs.length > 0) {// Servlet container verified the client certificate
+                       String certDn = certs[0].getSubjectX500Principal().getName();
+                       sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, certDn);
+                       sharedState.put(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN, certs);
+                       if (log.isDebugEnabled())
+                               log.debug("Client certificate " + certDn + " verified by servlet container");
+               } // Reverse proxy verified the client certificate
+               String clientDnHttpHeader = Activator.getHttpProxySslHeader();
+               if (clientDnHttpHeader != null) {
+                       String certDn = req.getHeader(clientDnHttpHeader);
+                       // TODO retrieve more cf. https://httpd.apache.org/docs/current/mod/mod_ssl.html
+                       // String issuerDn = req.getHeader("SSL_CLIENT_I_DN");
+                       if (certDn != null && !certDn.trim().equals("(null)")) {
+                               sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, certDn);
+                               sharedState.put(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN, "");
+                               if (log.isDebugEnabled())
+                                       log.debug("Client certificate " + certDn + " verified by reverse proxy");
+                       }
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/KeyringLoginModule.java
new file mode 100644 (file)
index 0000000..09fece0
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.auth;
+
+import java.security.AccessController;
+import java.util.Map;
+import java.util.Set;
+
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+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.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+import org.argeo.node.security.PBEKeySpecCallback;
+import org.argeo.util.PasswordEncryption;
+
+/** Adds a secret key to the private credentials */
+public class KeyringLoginModule implements LoginModule {
+       private Subject subject;
+       private CallbackHandler callbackHandler;
+       private SecretKey secretKey;
+
+       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+                       Map<String, ?> options) {
+               this.subject = subject;
+               if (subject == null) {
+                       subject = Subject.getSubject(AccessController.getContext());
+               }
+               this.callbackHandler = callbackHandler;
+       }
+
+       public boolean login() throws LoginException {
+//             Set<SecretKey> pbes = subject.getPrivateCredentials(SecretKey.class);
+//             if (pbes.size() > 0)
+//                     return true;
+               PasswordCallback pc = new PasswordCallback("Master password", false);
+               PBEKeySpecCallback pbeCb = new PBEKeySpecCallback();
+               Callback[] callbacks = { pc, pbeCb };
+               try {
+                       callbackHandler.handle(callbacks);
+                       char[] password = pc.getPassword();
+
+                       SecretKeyFactory keyFac = SecretKeyFactory.getInstance(pbeCb.getSecretKeyFactory());
+                       PBEKeySpec keySpec;
+                       if (pbeCb.getKeyLength() != null)
+                               keySpec = new PBEKeySpec(password, pbeCb.getSalt(), pbeCb.getIterationCount(), pbeCb.getKeyLength());
+                       else
+                               keySpec = new PBEKeySpec(password, pbeCb.getSalt(), pbeCb.getIterationCount());
+
+                       String secKeyEncryption = pbeCb.getSecretKeyEncryption();
+                       if (secKeyEncryption != null) {
+                               SecretKey tmp = keyFac.generateSecret(keySpec);
+                               secretKey = new SecretKeySpec(tmp.getEncoded(), secKeyEncryption);
+                       } else {
+                               secretKey = keyFac.generateSecret(keySpec);
+                       }
+               } catch (Exception e) {
+                       LoginException le = new LoginException("Cannot login keyring");
+                       le.initCause(e);
+                       throw le;
+               }
+               return true;
+       }
+
+       public boolean commit() throws LoginException {
+               if (secretKey != null) {
+                       subject.getPrivateCredentials().removeAll(subject.getPrivateCredentials(SecretKey.class));
+                       subject.getPrivateCredentials().add(secretKey);
+               }
+               return true;
+       }
+
+       public boolean abort() throws LoginException {
+               return true;
+       }
+
+       public boolean logout() throws LoginException {
+               Set<PasswordEncryption> pbes = subject.getPrivateCredentials(PasswordEncryption.class);
+               pbes.clear();
+               return true;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/SingleUserLoginModule.java
new file mode 100644 (file)
index 0000000..4d2cc33
--- /dev/null
@@ -0,0 +1,86 @@
+package org.argeo.cms.auth;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.Principal;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.ldap.LdapName;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.internal.auth.ImpliedByPrincipal;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.security.DataAdminPrincipal;
+import org.argeo.osgi.useradmin.IpaUtils;
+
+public class SingleUserLoginModule implements LoginModule {
+       private final static Log log = LogFactory.getLog(SingleUserLoginModule.class);
+
+       private Subject subject;
+       private Map<String, Object> sharedState = null;
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+                       Map<String, ?> options) {
+               this.subject = subject;
+               this.sharedState = (Map<String, Object>) sharedState;
+       }
+
+       @Override
+       public boolean login() throws LoginException {
+               String username = System.getProperty("user.name");
+               if (!sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME))
+                       sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, username);
+               return true;
+       }
+
+       @Override
+       public boolean commit() throws LoginException {
+               X500Principal principal;
+               KerberosPrincipal kerberosPrincipal = CmsAuthUtils.getSinglePrincipal(subject, KerberosPrincipal.class);
+               if (kerberosPrincipal != null) {
+                       LdapName userDn = IpaUtils.kerberosToDn(kerberosPrincipal.getName());
+                       principal = new X500Principal(userDn.toString());
+               } else {
+                       Object username = sharedState.get(CmsAuthUtils.SHARED_STATE_NAME);
+                       if (username == null)
+                               throw new LoginException("No username available");
+                       String hostname;
+                       try {
+                               hostname = InetAddress.getLocalHost().getHostName();
+                       } catch (UnknownHostException e) {
+                               log.warn("Using localhost as hostname", e);
+                               hostname = "localhost";
+                       }
+                       String baseDn = ("." + hostname).replaceAll("\\.", ",dc=");
+                       principal = new X500Principal(LdapAttrs.uid + "=" + username + baseDn);
+               }
+               Set<Principal> principals = subject.getPrincipals();
+               principals.add(principal);
+               principals.add(new ImpliedByPrincipal(NodeConstants.ROLE_ADMIN, principal));
+               principals.add(new DataAdminPrincipal());
+               return true;
+       }
+
+       @Override
+       public boolean abort() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean logout() throws LoginException {
+               // TODO Auto-generated method stub
+               return true;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/SpnegoLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/SpnegoLoginModule.java
new file mode 100644 (file)
index 0000000..27de54b
--- /dev/null
@@ -0,0 +1,135 @@
+package org.argeo.cms.auth;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.internal.kernel.Activator;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+
+/** SPNEGO login */
+public class SpnegoLoginModule implements LoginModule {
+       private final static Log log = LogFactory.getLog(SpnegoLoginModule.class);
+
+       private Subject subject;
+       private Map<String, Object> sharedState = null;
+
+       private GSSContext gssContext = null;
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+                       Map<String, ?> options) {
+               this.subject = subject;
+               this.sharedState = (Map<String, Object>) sharedState;
+       }
+
+       @Override
+       public boolean login() throws LoginException {
+               byte[] spnegoToken = (byte[]) sharedState.get(CmsAuthUtils.SHARED_STATE_SPNEGO_TOKEN);
+               if (spnegoToken == null)
+                       return false;
+               gssContext = checkToken(spnegoToken);
+               if (gssContext == null)
+                       return false;
+               else
+                       return true;
+               // try {
+               // String clientName = gssContext.getSrcName().toString();
+               // String role = clientName.substring(clientName.indexOf('@') + 1);
+               //
+               // log.debug("SpnegoUserRealm: established a security context");
+               // log.debug("Client Principal is: " + gssContext.getSrcName());
+               // log.debug("Server Principal is: " + gssContext.getTargName());
+               // log.debug("Client Default Role: " + role);
+               // } catch (GSSException e) {
+               // // TODO Auto-generated catch block
+               // e.printStackTrace();
+               // }
+       }
+
+       @Override
+       public boolean commit() throws LoginException {
+               if (gssContext == null)
+                       return false;
+
+               try {
+                       Class<?> gssUtilsClass = Class.forName("com.sun.security.jgss.GSSUtil");
+                       Method createSubjectMethod = gssUtilsClass.getMethod("createSubject", GSSName.class, GSSCredential.class);
+                       Subject gssSubject;
+                       if (gssContext.getCredDelegState())
+                               gssSubject = (Subject) createSubjectMethod.invoke(null, gssContext.getSrcName(),
+                                               gssContext.getDelegCred());
+                       else
+                               gssSubject = (Subject) createSubjectMethod.invoke(null, gssContext.getSrcName(), null);
+                       subject.getPrincipals().addAll(gssSubject.getPrincipals());
+                       subject.getPrivateCredentials().addAll(gssSubject.getPrivateCredentials());
+                       return true;
+               } catch (Exception e) {
+                       throw new LoginException("Cannot commit SPNEGO " + e);
+               }
+
+       }
+
+       @Override
+       public boolean abort() throws LoginException {
+               if (gssContext != null) {
+                       try {
+                               gssContext.dispose();
+                       } catch (GSSException e) {
+                               if (log.isTraceEnabled())
+                                       log.warn("Could not abort", e);
+                       }
+                       gssContext = null;
+               }
+               return true;
+       }
+
+       @Override
+       public boolean logout() throws LoginException {
+               if (gssContext != null) {
+                       try {
+                               gssContext.dispose();
+                       } catch (GSSException e) {
+                               if (log.isTraceEnabled())
+                                       log.warn("Could not abort", e);
+                       }
+                       gssContext = null;
+               }
+               return true;
+       }
+
+       private GSSContext checkToken(byte[] authToken) {
+               GSSManager manager = GSSManager.getInstance();
+               try {
+                       GSSContext gContext = manager.createContext(Activator.getAcceptorCredentials());
+
+                       if (gContext == null) {
+                               log.debug("SpnegoUserRealm: failed to establish GSSContext");
+                       } else {
+                               if (gContext.isEstablished())
+                                       return gContext;
+                               byte[] outToken = gContext.acceptSecContext(authToken, 0, authToken.length);
+                               if (outToken != null)
+                                       sharedState.put(CmsAuthUtils.SHARED_STATE_SPNEGO_OUT_TOKEN, outToken);
+                               if (gContext.isEstablished())
+                                       return gContext;
+                       }
+
+               } catch (GSSException gsse) {
+                       log.warn(gsse, gsse);
+               }
+               return null;
+
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java
new file mode 100644 (file)
index 0000000..ad9eb24
--- /dev/null
@@ -0,0 +1,351 @@
+package org.argeo.cms.auth;
+
+import static org.argeo.naming.LdapAttrs.cn;
+import static org.argeo.naming.LdapAttrs.description;
+
+import java.io.IOException;
+import java.security.PrivilegedAction;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.ldap.LdapName;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.LanguageCallback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.login.CredentialNotFoundException;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.internal.kernel.Activator;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.NamingUtils;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.security.CryptoKeyring;
+import org.argeo.osgi.useradmin.AuthenticatingUser;
+import org.argeo.osgi.useradmin.IpaUtils;
+import org.argeo.osgi.useradmin.OsUserUtils;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+public class UserAdminLoginModule implements LoginModule {
+       private final static Log log = LogFactory.getLog(UserAdminLoginModule.class);
+
+       private Subject subject;
+       private CallbackHandler callbackHandler;
+       private Map<String, Object> sharedState = null;
+
+       private List<String> indexedUserProperties = Arrays
+                       .asList(new String[] { LdapAttrs.mail.name(), LdapAttrs.uid.name(), LdapAttrs.authPassword.name() });
+
+       // private state
+       private BundleContext bc;
+       private User authenticatedUser = null;
+       private Locale locale;
+
+       private Authorization bindAuthorization = null;
+
+       private boolean singleUser = Activator.isSingleUser();
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+                       Map<String, ?> options) {
+               this.subject = subject;
+               try {
+                       bc = FrameworkUtil.getBundle(UserAdminLoginModule.class).getBundleContext();
+                       this.callbackHandler = callbackHandler;
+                       this.sharedState = (Map<String, Object>) sharedState;
+               } catch (Exception e) {
+                       throw new CmsException("Cannot initialize login module", e);
+               }
+       }
+
+       @Override
+       public boolean login() throws LoginException {
+               UserAdmin userAdmin = Activator.getUserAdmin();
+               final String username;
+               final char[] password;
+               Object certificateChain = null;
+               if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME)
+                               && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_PWD)) {
+                       // NB: required by Basic http auth
+                       username = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME);
+                       password = (char[]) sharedState.get(CmsAuthUtils.SHARED_STATE_PWD);
+                       // // TODO locale?
+               } else if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME)
+                               && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN)) {
+                       String certDn = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME);
+//                     LdapName ldapName;
+//                     try {
+//                             ldapName = new LdapName(certificateName);
+//                     } catch (InvalidNameException e) {
+//                             e.printStackTrace();
+//                             return false;
+//                     }
+//                     username = ldapName.getRdn(ldapName.size() - 1).getValue().toString();
+                       username = certDn;
+                       certificateChain = sharedState.get(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN);
+                       password = null;
+               } else if (singleUser) {
+                       username = OsUserUtils.getOsUsername();
+                       password = null;
+               } else {
+
+                       // ask for username and password
+                       NameCallback nameCallback = new NameCallback("User");
+                       PasswordCallback passwordCallback = new PasswordCallback("Password", false);
+                       LanguageCallback langCallback = new LanguageCallback();
+                       try {
+                               callbackHandler.handle(new Callback[] { nameCallback, passwordCallback, langCallback });
+                       } catch (IOException e) {
+                               throw new LoginException("Cannot handle callback: " + e.getMessage());
+                       } catch (UnsupportedCallbackException e) {
+                               return false;
+                       }
+
+                       // i18n
+                       locale = langCallback.getLocale();
+                       if (locale == null)
+                               locale = Locale.getDefault();
+                       // FIXME add it to Subject
+                       // Locale.setDefault(locale);
+
+                       username = nameCallback.getName();
+                       if (username == null || username.trim().equals("")) {
+                               // authorization = userAdmin.getAuthorization(null);
+                               throw new CredentialNotFoundException("No credentials provided");
+                       }
+                       if (passwordCallback.getPassword() != null)
+                               password = passwordCallback.getPassword();
+                       else
+                               throw new CredentialNotFoundException("No credentials provided");
+                       sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, username);
+                       sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, password);
+               }
+               User user = searchForUser(userAdmin, username);
+
+               // Tokens
+               if (user == null) {
+                       String token = username;
+                       Group tokenGroup = searchForToken(userAdmin, token);
+                       if (tokenGroup != null) {
+                               Authorization tokenAuthorization = getAuthorizationFromToken(userAdmin, tokenGroup);
+                               if (tokenAuthorization != null) {
+                                       bindAuthorization = tokenAuthorization;
+                                       authenticatedUser = (User) userAdmin.getRole(bindAuthorization.getName());
+                                       return true;
+                               }
+                       }
+               }
+
+               if (user == null)
+                       return true;// expect Kerberos
+
+               if (password != null) {
+                       // try bind first
+                       try {
+                               AuthenticatingUser authenticatingUser = new AuthenticatingUser(user.getName(), password);
+                               bindAuthorization = userAdmin.getAuthorization(authenticatingUser);
+                               // TODO check tokens as well
+                               if (bindAuthorization != null) {
+                                       authenticatedUser = user;
+                                       return true;
+                               }
+                       } catch (Exception e) {
+                               // silent
+                               if (log.isTraceEnabled())
+                                       log.trace("Bind failed", e);
+                       }
+
+                       // works only if a connection password is provided
+                       if (!user.hasCredential(null, password)) {
+                               return false;
+                       }
+               } else if (certificateChain != null) {
+                       // TODO check CRLs/OSCP validity?
+                       // NB: authorization in commit() will work only if an LDAP connection password
+                       // is provided
+               } else if (singleUser) {
+                       // TODO verify IP address?
+               } else {
+                       throw new CredentialNotFoundException("No credentials provided");
+               }
+
+               authenticatedUser = user;
+               return true;
+       }
+
+       @Override
+       public boolean commit() throws LoginException {
+               if (locale == null)
+                       subject.getPublicCredentials().add(Locale.getDefault());
+               else
+                       subject.getPublicCredentials().add(locale);
+
+               if (singleUser) {
+                       OsUserUtils.loginAsSystemUser(subject);
+               }
+               UserAdmin userAdmin = Activator.getUserAdmin();
+               Authorization authorization;
+               if (callbackHandler == null) {// anonymous
+                       authorization = userAdmin.getAuthorization(null);
+               } else if (bindAuthorization != null) {// bind
+                       authorization = bindAuthorization;
+               } else {// Kerberos
+                       User authenticatingUser;
+                       Set<KerberosPrincipal> kerberosPrincipals = subject.getPrincipals(KerberosPrincipal.class);
+                       if (kerberosPrincipals.isEmpty()) {
+                               if (authenticatedUser == null) {
+                                       if (log.isTraceEnabled())
+                                               log.trace("Neither kerberos nor user admin login succeeded. Login failed.");
+                                       return false;
+                               } else {
+                                       authenticatingUser = authenticatedUser;
+                               }
+                       } else {
+                               KerberosPrincipal kerberosPrincipal = kerberosPrincipals.iterator().next();
+                               LdapName dn = IpaUtils.kerberosToDn(kerberosPrincipal.getName());
+                               authenticatingUser = new AuthenticatingUser(dn);
+                               if (authenticatedUser != null && !authenticatingUser.getName().equals(authenticatedUser.getName()))
+                                       throw new LoginException("Kerberos login " + authenticatingUser.getName()
+                                                       + " is inconsistent with user admin login " + authenticatedUser.getName());
+                       }
+                       authorization = Subject.doAs(subject, new PrivilegedAction<Authorization>() {
+
+                               @Override
+                               public Authorization run() {
+                                       Authorization authorization = userAdmin.getAuthorization(authenticatingUser);
+                                       return authorization;
+                               }
+
+                       });
+                       if (authorization == null)
+                               throw new LoginException(
+                                               "User admin found no authorization for authenticated user " + authenticatingUser.getName());
+               }
+
+               // Log and monitor new login
+               HttpServletRequest request = (HttpServletRequest) sharedState.get(CmsAuthUtils.SHARED_STATE_HTTP_REQUEST);
+               CmsAuthUtils.addAuthorization(subject, authorization, locale, request);
+
+               // Unlock keyring (underlying login to the JCR repository)
+               char[] password = (char[]) sharedState.get(CmsAuthUtils.SHARED_STATE_PWD);
+               if (password != null) {
+                       ServiceReference<CryptoKeyring> keyringSr = bc.getServiceReference(CryptoKeyring.class);
+                       if (keyringSr != null) {
+                               CryptoKeyring keyring = bc.getService(keyringSr);
+                               Subject.doAs(subject, new PrivilegedAction<Void>() {
+
+                                       @Override
+                                       public Void run() {
+                                               try {
+                                                       keyring.unlock(password);
+                                               } catch (Exception e) {
+                                                       e.printStackTrace();
+                                                       log.warn("Could not unlock keyring with the password provided by " + authorization.getName()
+                                                                       + ": " + e.getMessage());
+                                               }
+                                               return null;
+                                       }
+
+                               });
+                       }
+               }
+
+               // Register CmsSession with initial subject
+               CmsAuthUtils.registerSessionAuthorization(request, subject, authorization, locale);
+
+               if (log.isDebugEnabled())
+                       log.debug("Logged in to CMS: " + subject);
+               return true;
+       }
+
+       @Override
+       public boolean abort() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean logout() throws LoginException {
+               if (log.isTraceEnabled())
+                       log.trace("Logging out from CMS... " + subject);
+               // boolean httpSessionLogoutOk = CmsAuthUtils.logoutSession(bc,
+               // subject);
+               CmsAuthUtils.cleanUp(subject);
+               return true;
+       }
+
+       protected User searchForUser(UserAdmin userAdmin, String providedUsername) {
+               try {
+                       // TODO check value null or empty
+                       Set<User> collectedUsers = new HashSet<>();
+                       // try dn
+                       User user = null;
+                       // try all indexes
+                       for (String attr : indexedUserProperties) {
+                               user = userAdmin.getUser(attr, providedUsername);
+                               if (user != null)
+                                       collectedUsers.add(user);
+                       }
+                       if (collectedUsers.size() == 1) {
+                               user = collectedUsers.iterator().next();
+                               return user;
+                       } else if (collectedUsers.size() > 1) {
+                               log.warn(collectedUsers.size() + " users for provided username" + providedUsername);
+                       }
+                       // try DN as a last resort
+                       try {
+                               user = (User) userAdmin.getRole(providedUsername);
+                               if (user != null)
+                                       return user;
+                       } catch (Exception e) {
+                               // silent
+                       }
+                       return null;
+               } catch (Exception e) {
+                       if (log.isTraceEnabled())
+                               log.warn("Cannot search for user " + providedUsername, e);
+                       return null;
+               }
+
+       }
+
+       protected Group searchForToken(UserAdmin userAdmin, String token) {
+               String dn = cn + "=" + token + "," + NodeConstants.TOKENS_BASEDN;
+               Group tokenGroup = (Group) userAdmin.getRole(dn);
+               return tokenGroup;
+       }
+
+       protected Authorization getAuthorizationFromToken(UserAdmin userAdmin, Group tokenGroup) {
+               String expiryDateStr = (String) tokenGroup.getProperties().get(description.name());
+               if (expiryDateStr != null) {
+                       Instant expiryDate = NamingUtils.ldapDateToInstant(expiryDateStr);
+                       if (expiryDate.isBefore(Instant.now())) {
+                               if (log.isDebugEnabled())
+                                       log.debug("Token " + tokenGroup.getName() + " has expired.");
+                               return null;
+                       }
+               }
+               Authorization auth = userAdmin.getAuthorization(tokenGroup);
+               return auth;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/cmd/Sync.java b/org.argeo.cms/src/org/argeo/cms/cmd/Sync.java
new file mode 100644 (file)
index 0000000..515ef6c
--- /dev/null
@@ -0,0 +1,76 @@
+package org.argeo.cms.cmd;
+
+import java.net.URI;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.spi.FileSystemProvider;
+import java.time.ZonedDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.jackrabbit.fs.DavexFsProvider;
+import org.argeo.util.LangUtils;
+
+public class Sync {
+       private final static Log log = LogFactory.getLog(Sync.class);
+
+       public static void main(String args[]) {
+               Map<String, String> arguments = new HashMap<>();
+               boolean skipNext = false;
+               String currValue = null;
+               for (int i = 0; i < args.length; i++) {
+                       if (skipNext) {
+                               skipNext = false;
+                               currValue = null;
+                               continue;
+                       }
+                       String arg = args[i];
+                       if (arg.startsWith("-")) {
+                               if (i + 1 < args.length) {
+                                       if (!args[i + 1].startsWith("-")) {
+                                               currValue = args[i + 1];
+                                               skipNext = true;
+                                       }
+                               }
+                               arguments.put(arg, currValue);
+                       } else {
+                               // TODO add multiple?
+                       }
+               }
+
+               try {
+                       URI sourceUri = new URI(arguments.get("-i"));
+                       URI targetUri = new URI(arguments.get("-o"));
+                       FileSystemProvider sourceFsProvider = createFsProvider(sourceUri);
+                       FileSystemProvider targetFsProvider = createFsProvider(targetUri);
+                       Path sourceBasePath = sourceFsProvider.getPath(sourceUri);
+                       Path targetBasePath = targetFsProvider.getPath(targetUri);
+                       SyncFileVisitor syncFileVisitor = new SyncFileVisitor(sourceBasePath, targetBasePath);
+                       ZonedDateTime begin = ZonedDateTime.now();
+                       Files.walkFileTree(sourceBasePath, syncFileVisitor);
+                       if (log.isDebugEnabled())
+                               log.debug("Sync from " + sourceBasePath + " to " + targetBasePath + " took " + LangUtils.since(begin));
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+
+       private static FileSystemProvider createFsProvider(URI uri) {
+               FileSystemProvider fsProvider;
+               if (uri.getScheme().equals("file"))
+                       fsProvider = FileSystems.getDefault().provider();
+               else if (uri.getScheme().equals("davex"))
+                       fsProvider = new DavexFsProvider();
+               else
+                       throw new CmsException("URI scheme not supported for " + uri);
+               return fsProvider;
+       }
+
+       static enum Arg {
+               to, from;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/cmd/SyncFileVisitor.java b/org.argeo.cms/src/org/argeo/cms/cmd/SyncFileVisitor.java
new file mode 100644 (file)
index 0000000..6ec75f4
--- /dev/null
@@ -0,0 +1,56 @@
+package org.argeo.cms.cmd;
+
+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;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/** Synchronises two directory structures. */
+public class SyncFileVisitor extends SimpleFileVisitor<Path> {
+       private final static Log log = LogFactory.getLog(SyncFileVisitor.class);
+
+       private final Path sourceBasePath;
+       private final Path targetBasePath;
+
+       public SyncFileVisitor(Path sourceBasePath, Path targetBasePath) {
+               this.sourceBasePath = sourceBasePath;
+               this.targetBasePath = targetBasePath;
+       }
+
+       @Override
+       public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+               Path targetPath = toTargetPath(dir);
+               Files.createDirectories(targetPath);
+               return FileVisitResult.CONTINUE;
+       }
+
+       @Override
+       public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+               Path targetPath = toTargetPath(file);
+               try {
+                       Files.copy(file, targetPath);
+                       if (log.isDebugEnabled())
+                               log.debug("Copied " + targetPath);
+               } catch (Exception e) {
+                       log.error("Cannot copy " + file + " to " + targetPath, e);
+               }
+               return FileVisitResult.CONTINUE;
+       }
+
+       @Override
+       public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
+               log.error("Cannot sync " + file, exc);
+               return FileVisitResult.CONTINUE;
+       }
+
+       private Path toTargetPath(Path sourcePath) {
+               Path relativePath = sourceBasePath.relativize(sourcePath);
+               Path targetPath = targetBasePath.resolve(relativePath.toString());
+               return targetPath;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/cms.cnd b/org.argeo.cms/src/org/argeo/cms/cms.cnd
new file mode 100644 (file)
index 0000000..955a4a3
--- /dev/null
@@ -0,0 +1,56 @@
+<argeo = 'http://www.argeo.org/ns/argeo'>
+<cms = 'http://www.argeo.org/ns/cms'>
+
+// GENERIC TYPES
+[argeo:remoteRepository] > nt:unstructured
+- argeo:uri (STRING)
+- argeo:userID (STRING)
++ argeo:password (argeo:encrypted)
+
+// TABULAR CONTENT
+[argeo:table] > nt:file
++ * (argeo:column) *
+
+[argeo:column] > mix:title
+- jcr:requiredType (STRING) = 'STRING'
+
+[argeo:csv] > nt:resource
+
+// CRYPTO
+[argeo:encrypted] > nt:base
+mixin
+// initialization vector used by some algorithms
+- argeo:iv (BINARY)
+
+[argeo:pbeKeySpec] > nt:base
+mixin
+- argeo:secretKeyFactory (STRING)
+- argeo:salt (BINARY)
+- argeo:iterationCount (LONG)
+- argeo:keyLength (LONG)
+- argeo:secretKeyEncryption (STRING)
+
+[argeo:pbeSpec] > argeo:pbeKeySpec
+mixin
+- argeo:cipher (STRING)
+
+// TEXT
+[cms:styled]
+mixin
+- cms:style (STRING)
+- cms:content (STRING)
+- cms:data (BINARY)
+
+[cms:image] > mix:title, mix:mimeType
+mixin
+- cms:imageWidth (STRING)
+- cms:imageHeight (STRING)
+
+[cms:section] > nt:folder, mix:created, mix:lastModified, mix:title
+orderable
++ cms:p (nt:base) = nt:unstructured * 
++ cms:h (cms:section) *
++ cms:attached (nt:folder)
+
+[cms:text] > cms:section
++ cms:history (nt:folder)
diff --git a/org.argeo.cms/src/org/argeo/cms/i18n/DefaultsResourceBundle.java b/org.argeo.cms/src/org/argeo/cms/i18n/DefaultsResourceBundle.java
new file mode 100644 (file)
index 0000000..78d717a
--- /dev/null
@@ -0,0 +1,42 @@
+package org.argeo.cms.i18n;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Enumeration;
+import java.util.ResourceBundle;
+import java.util.Vector;
+
+import org.argeo.cms.CmsException;
+
+/** Expose the default values as a {@link ResourceBundle} */
+@Deprecated
+public class DefaultsResourceBundle extends ResourceBundle {
+
+       @Override
+       protected Object handleGetObject(String key) {
+               Object obj;
+               try {
+                       Field field = getClass().getField(key);
+                       obj = field.getType().getMethod("getDefault")
+                                       .invoke(field.get(null));
+               } catch (Exception e) {
+                       throw new CmsException("Cannot get default for " + key, e);
+               }
+               return obj;
+       }
+
+       @Override
+       public Enumeration<String> getKeys() {
+               Vector<String> res = new Vector<String>();
+               final Field[] fieldArray = getClass().getDeclaredFields();
+
+               for (Field field : fieldArray) {
+                       if (Modifier.isStatic(field.getModifiers())
+                                       && field.getType().isAssignableFrom(LocaleUtils.class)) {
+                               res.add(field.getName());
+                       }
+               }
+               return res.elements();
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/i18n/LocaleUtils.java b/org.argeo.cms/src/org/argeo/cms/i18n/LocaleUtils.java
new file mode 100644 (file)
index 0000000..e0a77bc
--- /dev/null
@@ -0,0 +1,70 @@
+package org.argeo.cms.i18n;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.ResourceBundle;
+
+import org.argeo.cms.auth.CurrentUser;
+
+/** Utilities simplifying the development of localization enums. */
+public class LocaleUtils {
+       public static Object local(Enum<?> en) {
+               return local(en, getCurrentLocale(), "/OSGI-INF/l10n/bundle");
+       }
+
+       public static Object local(Enum<?> en, Locale locale) {
+               return local(en, locale, "/OSGI-INF/l10n/bundle");
+       }
+
+       public static Object local(Enum<?> en, Locale locale, String resource) {
+               return local(en, locale, resource, en.getClass().getClassLoader());
+       }
+
+       public static Object local(Enum<?> en, Locale locale, String resource, ClassLoader classLoader) {
+               ResourceBundle rb = ResourceBundle.getBundle(resource, locale, classLoader);
+               return rb.getString(en.name());
+       }
+
+       public static String lead(String raw, Locale locale) {
+               return raw.substring(0, 1).toUpperCase(locale) + raw.substring(1);
+       }
+
+       public static String lead(Localized localized) {
+               return lead(localized, getCurrentLocale());
+       }
+
+       public static String lead(Localized localized, Locale locale) {
+               return lead(localized.local(locale).toString(), locale);
+       }
+
+       static Locale getCurrentLocale() {
+               return CurrentUser.locale();
+               // return UiContext.getLocale();
+               // FIXME look into Subject or settings
+               // return Locale.getDefault();
+       }
+
+       /** Returns null if argument is null. */
+       public static List<Locale> asLocaleList(Object locales) {
+               if (locales == null)
+                       return null;
+               ArrayList<Locale> availableLocales = new ArrayList<Locale>();
+               String[] codes = locales.toString().split(",");
+               for (int i = 0; i < codes.length; i++) {
+                       String code = codes[i];
+                       // variant not supported
+                       int indexUnd = code.indexOf("_");
+                       Locale locale;
+                       if (indexUnd > 0) {
+                               String language = code.substring(0, indexUnd);
+                               String country = code.substring(indexUnd + 1);
+                               locale = new Locale(language, country);
+                       } else {
+                               locale = new Locale(code);
+                       }
+                       availableLocales.add(locale);
+               }
+               return availableLocales;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/i18n/Localized.java b/org.argeo.cms/src/org/argeo/cms/i18n/Localized.java
new file mode 100644 (file)
index 0000000..535b5f2
--- /dev/null
@@ -0,0 +1,27 @@
+package org.argeo.cms.i18n;
+
+import java.text.MessageFormat;
+import java.util.Locale;
+
+/** Localized object. */
+public interface Localized {
+       /** Default assumes that this is an {@link Enum} */
+       default Object local(Locale locale) {
+               return LocaleUtils.local((Enum<?>) this, locale);
+       }
+
+       default String lead() {
+               return LocaleUtils.lead(this);
+       }
+
+       default String format(Object[] args) {
+               Locale locale = LocaleUtils.getCurrentLocale();
+               MessageFormat format = new MessageFormat(local(locale).toString(), locale);
+               return format.format(args);
+       }
+
+       default String lead(Locale locale) {
+               return LocaleUtils.lead(local(locale).toString(), locale);
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsSessionImpl.java
new file mode 100644 (file)
index 0000000..82a6972
--- /dev/null
@@ -0,0 +1,303 @@
+package org.argeo.cms.internal.auth;
+
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedExceptionAction;
+import java.time.ZonedDateTime;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+import javax.crypto.SecretKey;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CmsSession;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.security.NodeSecurityUtils;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.useradmin.Authorization;
+
+public class CmsSessionImpl implements CmsSession {
+       private final static BundleContext bc = FrameworkUtil.getBundle(CmsSessionImpl.class).getBundleContext();
+       private final static Log log = LogFactory.getLog(CmsSessionImpl.class);
+
+       // private final Subject initialSubject;
+       private final AccessControlContext initialContext;
+       private final UUID uuid;
+       private final String localSessionId;
+       private final Authorization authorization;
+       private final LdapName userDn;
+       private final boolean anonymous;
+
+       private final ZonedDateTime creationTime;
+       private ZonedDateTime end;
+       private final Locale locale;
+
+       private ServiceRegistration<CmsSession> serviceRegistration;
+
+       private Map<String, Session> dataSessions = new HashMap<>();
+       private Set<String> dataSessionsInUse = new HashSet<>();
+       private LinkedHashSet<Session> additionalDataSessions = new LinkedHashSet<>();
+
+       public CmsSessionImpl(Subject initialSubject, Authorization authorization, Locale locale, String localSessionId) {
+               this.creationTime = ZonedDateTime.now();
+               this.locale = locale;
+               this.initialContext = Subject.doAs(initialSubject, new PrivilegedAction<AccessControlContext>() {
+
+                       @Override
+                       public AccessControlContext run() {
+                               return AccessController.getContext();
+                       }
+
+               });
+               // this.initialSubject = initialSubject;
+               this.localSessionId = localSessionId;
+               this.authorization = authorization;
+               if (authorization.getName() != null)
+                       try {
+                               this.userDn = new LdapName(authorization.getName());
+                               this.anonymous = false;
+                       } catch (InvalidNameException e) {
+                               throw new CmsException("Invalid user name " + authorization.getName(), e);
+                       }
+               else {
+                       this.userDn = NodeSecurityUtils.ROLE_ANONYMOUS_NAME;
+                       this.anonymous = true;
+               }
+               this.uuid = UUID.randomUUID();
+               // register as service
+               Hashtable<String, String> props = new Hashtable<>();
+               props.put(CmsSession.USER_DN, userDn.toString());
+               props.put(CmsSession.SESSION_UUID, uuid.toString());
+               props.put(CmsSession.SESSION_LOCAL_ID, localSessionId);
+               serviceRegistration = bc.registerService(CmsSession.class, this, props);
+       }
+
+       public void close() {
+               end = ZonedDateTime.now();
+               serviceRegistration.unregister();
+
+               synchronized (this) {
+                       // TODO check data session in use ?
+                       for (String path : dataSessions.keySet())
+                               JcrUtils.logoutQuietly(dataSessions.get(path));
+                       for (Session session : additionalDataSessions)
+                               JcrUtils.logoutQuietly(session);
+               }
+
+               try {
+                       LoginContext lc;
+                       if (isAnonymous()) {
+                               lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS, getSubject());
+                       } else {
+                               lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, getSubject());
+                       }
+                       lc.logout();
+               } catch (LoginException e) {
+                       log.warn("Could not logout " + getSubject() + ": " + e);
+               }
+               log.debug("Closed " + this);
+       }
+
+       private Subject getSubject() {
+               return Subject.getSubject(initialContext);
+       }
+
+       public Set<SecretKey> getSecretKeys() {
+               return getSubject().getPrivateCredentials(SecretKey.class);
+       }
+
+       public synchronized Session getDataSession(String cn, String workspace, Repository repository) {
+               // FIXME make it more robust
+               if (workspace == null)
+                       workspace = "main";
+               String path = cn + '/' + workspace;
+               if (dataSessionsInUse.contains(path)) {
+                       try {
+                               wait(1000);
+                               if (dataSessionsInUse.contains(path)) {
+                                       Session session = login(repository, workspace);
+                                       additionalDataSessions.add(session);
+                                       if (log.isTraceEnabled())
+                                               log.trace("Additional data session " + path + " for " + userDn);
+                                       return session;
+                               }
+                       } catch (InterruptedException e) {
+                               // silent
+                       }
+               }
+
+               Session session = null;
+               if (dataSessions.containsKey(path)) {
+                       session = dataSessions.get(path);
+               } else {
+                       session = login(repository, workspace);
+                       dataSessions.put(path, session);
+                       if (log.isTraceEnabled())
+                               log.trace("New data session " + path + " for " + userDn);
+               }
+               dataSessionsInUse.add(path);
+               return session;
+       }
+
+       private Session login(Repository repository, String workspace) {
+               try {
+                       return Subject.doAs(getSubject(), new PrivilegedExceptionAction<Session>() {
+                               @Override
+                               public Session run() throws Exception {
+                                       return repository.login(workspace);
+                               }
+                       });
+               } catch (Exception e) {
+                       throw new CmsException("Cannot log in " + userDn + " to JCR", e);
+               }
+       }
+
+       public synchronized void releaseDataSession(String cn, Session session) {
+               if (additionalDataSessions.contains(session)) {
+                       JcrUtils.logoutQuietly(session);
+                       additionalDataSessions.remove(session);
+                       if (log.isTraceEnabled())
+                               log.trace("Remove additional data session " + session);
+                       return;
+               }
+               String path = cn + '/' + session.getWorkspace().getName();
+               if (!dataSessionsInUse.contains(path))
+                       log.warn("Data session " + path + " was not in use for " + userDn);
+               dataSessionsInUse.remove(path);
+               Session registeredSession = dataSessions.get(path);
+               if (session != registeredSession)
+                       log.warn("Data session " + path + " not consistent for " + userDn);
+               if (log.isTraceEnabled())
+                       log.trace("Released data session " + session + " for " + path);
+               notifyAll();
+       }
+
+       @Override
+       public boolean isValid() {
+               return !isClosed();
+       }
+
+       protected boolean isClosed() {
+               return getEnd() != null;
+       }
+
+       @Override
+       public Authorization getAuthorization() {
+               return authorization;
+       }
+
+       @Override
+       public UUID getUuid() {
+               return uuid;
+       }
+
+       @Override
+       public LdapName getUserDn() {
+               return userDn;
+       }
+
+       @Override
+       public String getLocalId() {
+               return localSessionId;
+       }
+
+       @Override
+       public boolean isAnonymous() {
+               return anonymous;
+       }
+
+       @Override
+       public Locale getLocale() {
+               return locale;
+       }
+
+       @Override
+       public ZonedDateTime getCreationTime() {
+               return creationTime;
+       }
+
+       @Override
+       public ZonedDateTime getEnd() {
+               return end;
+       }
+
+       public String toString() {
+               return "CMS Session " + userDn + " local=" + localSessionId + ", uuid=" + uuid;
+       }
+
+       public static CmsSessionImpl getByLocalId(String localId) {
+               Collection<ServiceReference<CmsSession>> sr;
+               try {
+                       sr = bc.getServiceReferences(CmsSession.class, "(" + CmsSession.SESSION_LOCAL_ID + "=" + localId + ")");
+               } catch (InvalidSyntaxException e) {
+                       throw new CmsException("Cannot get CMS session for id " + localId, e);
+               }
+               ServiceReference<CmsSession> cmsSessionRef;
+               if (sr.size() == 1) {
+                       cmsSessionRef = sr.iterator().next();
+                       return (CmsSessionImpl) bc.getService(cmsSessionRef);
+               } else if (sr.size() == 0) {
+                       return null;
+               } else
+                       throw new CmsException(sr.size() + " CMS sessions registered for " + localId);
+
+       }
+
+       public static CmsSessionImpl getByUuid(Object uuid) {
+               Collection<ServiceReference<CmsSession>> sr;
+               try {
+                       sr = bc.getServiceReferences(CmsSession.class, "(" + CmsSession.SESSION_UUID + "=" + uuid + ")");
+               } catch (InvalidSyntaxException e) {
+                       throw new CmsException("Cannot get CMS session for uuid " + uuid, e);
+               }
+               ServiceReference<CmsSession> cmsSessionRef;
+               if (sr.size() == 1) {
+                       cmsSessionRef = sr.iterator().next();
+                       return (CmsSessionImpl) bc.getService(cmsSessionRef);
+               } else if (sr.size() == 0) {
+                       return null;
+               } else
+                       throw new CmsException(sr.size() + " CMS sessions registered for " + uuid);
+
+       }
+
+       public static void closeInvalidSessions() {
+               Collection<ServiceReference<CmsSession>> srs;
+               try {
+                       srs = bc.getServiceReferences(CmsSession.class, null);
+                       for (ServiceReference<CmsSession> sr : srs) {
+                               CmsSession cmsSession = bc.getService(sr);
+                               if (!cmsSession.isValid()) {
+                                       ((CmsSessionImpl) cmsSession).close();
+                                       if (log.isDebugEnabled())
+                                               log.debug("Closed expired CMS session " + cmsSession);
+                               }
+                       }
+               } catch (InvalidSyntaxException e) {
+                       throw new CmsException("Cannot get CMS sessions", e);
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/ConsoleCallbackHandler.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/ConsoleCallbackHandler.java
new file mode 100644 (file)
index 0000000..4f1d363
--- /dev/null
@@ -0,0 +1,69 @@
+package org.argeo.cms.internal.auth;
+
+import java.io.Console;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Arrays;
+
+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.TextOutputCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import org.argeo.cms.CmsException;
+
+/** Callback handler to be used with a command line UI. */
+public class ConsoleCallbackHandler implements CallbackHandler {
+
+       @Override
+       public void handle(Callback[] callbacks) throws IOException,
+                       UnsupportedCallbackException {
+               Console console = System.console();
+               if (console == null)
+                       throw new CmsException("No console available");
+
+               PrintWriter writer = console.writer();
+               for (int i = 0; i < callbacks.length; i++) {
+                       if (callbacks[i] instanceof TextOutputCallback) {
+                               TextOutputCallback callback = (TextOutputCallback) callbacks[i];
+                               writer.write(callback.getMessage());
+                       } else if (callbacks[i] instanceof NameCallback) {
+                               NameCallback callback = (NameCallback) callbacks[i];
+                               writer.write(callback.getPrompt());
+                               if (callback.getDefaultName() != null)
+                                       writer.write(" (" + callback.getDefaultName() + ")");
+                               writer.write(" : ");
+                               String answer = console.readLine();
+                               if (callback.getDefaultName() != null
+                                               && answer.trim().equals(""))
+                                       callback.setName(callback.getDefaultName());
+                               else
+                                       callback.setName(answer);
+                       } else if (callbacks[i] instanceof PasswordCallback) {
+                               PasswordCallback callback = (PasswordCallback) callbacks[i];
+                               writer.write(callback.getPrompt());
+                               char[] answer = console.readPassword();
+                               callback.setPassword(answer);
+                               Arrays.fill(answer, ' ');
+                       }
+//                     else if (callbacks[i] instanceof LocaleChoice) {
+//                             LocaleChoice callback = (LocaleChoice) callbacks[i];
+//                             writer.write("Language");
+//                             writer.write("\n");
+//                             for (int j = 0; j < callback.getLocales().size(); j++) {
+//                                     Locale locale = callback.getLocales().get(j);
+//                                     writer.print(j + " : " + locale.getDisplayName() + "\n");
+//                             }
+//                             writer.write("(" + callback.getDefaultIndex() + ") : ");
+//                             String answer = console.readLine();
+//                             if (answer.trim().equals(""))
+//                                     callback.setSelectedIndex(callback.getDefaultIndex());
+//                             else
+//                                     callback.setSelectedIndex(new Integer(answer.trim()));
+//                     }
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/ImpliedByPrincipal.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/ImpliedByPrincipal.java
new file mode 100644 (file)
index 0000000..6f83a9a
--- /dev/null
@@ -0,0 +1,91 @@
+package org.argeo.cms.internal.auth;
+
+import java.security.Principal;
+import java.security.acl.Group;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.cms.CmsException;
+import org.osgi.service.useradmin.Authorization;
+
+/**
+ * A {@link Principal} which has been implied by an {@link Authorization}. If it
+ * is empty it meeans this is an additional identity, otherwise it lists the
+ * users (typically the logged in user but possibly empty
+ * {@link ImpliedByPrincipal}s) which have implied it. When an additional
+ * identityx is removed, the related {@link ImpliedByPrincipal}s can thus be
+ * removed.
+ */
+public final class ImpliedByPrincipal implements Group {
+       private final LdapName name;
+       private Set<Principal> causes = new HashSet<Principal>();
+
+       public ImpliedByPrincipal(String name, Principal userPrincipal) {
+               try {
+                       this.name = new LdapName(name);
+               } catch (InvalidNameException e) {
+                       throw new CmsException("Badly formatted role name", e);
+               }
+               if (userPrincipal != null)
+                       causes.add(userPrincipal);
+       }
+
+       public ImpliedByPrincipal(LdapName name, Principal userPrincipal) {
+               this.name = name;
+               if (userPrincipal != null)
+                       causes.add(userPrincipal);
+       }
+
+       @Override
+       public String getName() {
+               return name.toString();
+       }
+
+       @Override
+       public boolean addMember(Principal user) {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public boolean removeMember(Principal user) {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public boolean isMember(Principal member) {
+               return causes.contains(member);
+       }
+
+       @Override
+       public Enumeration<? extends Principal> members() {
+               return Collections.enumeration(causes);
+       }
+
+       @Override
+       public int hashCode() {
+               return name.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               // if (this == obj)
+               // return true;
+               if (obj instanceof ImpliedByPrincipal) {
+                       ImpliedByPrincipal that = (ImpliedByPrincipal) obj;
+                       // TODO check members too?
+                       return name.equals(that.name);
+               }
+               return false;
+       }
+
+       @Override
+       public String toString() {
+               // return name.toString() + " implied by " + causes;
+               return name.toString();
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java b/org.argeo.cms/src/org/argeo/cms/internal/http/CmsSessionProvider.java
new file mode 100644 (file)
index 0000000..a1ddcb0
--- /dev/null
@@ -0,0 +1,81 @@
+package org.argeo.cms.internal.http;
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.server.SessionProvider;
+import org.argeo.cms.internal.auth.CmsSessionImpl;
+import org.argeo.jcr.JcrUtils;
+
+/**
+ * Implements an open session in view patter: a new JCR session is created for
+ * each request
+ */
+public class CmsSessionProvider implements SessionProvider, Serializable {
+       private static final long serialVersionUID = -1358136599534938466L;
+
+       private final static Log log = LogFactory.getLog(CmsSessionProvider.class);
+
+       private final String alias;
+
+       private LinkedHashMap<Session, CmsSessionImpl> cmsSessions = new LinkedHashMap<>();
+
+       public CmsSessionProvider(String alias) {
+               this.alias = alias;
+       }
+
+       public Session getSession(HttpServletRequest request, Repository rep, String workspace)
+                       throws javax.jcr.LoginException, ServletException, RepositoryException {
+
+               CmsSessionImpl cmsSession = WebCmsSessionImpl.getCmsSession(request);
+               // if (cmsSession == null)
+               // return anonymousSession(request, rep, workspace);
+               if (log.isTraceEnabled()) {
+                       log.trace("Get JCR session from " + cmsSession);
+               }
+               Session session = cmsSession.getDataSession(alias, workspace, rep);
+               cmsSessions.put(session, cmsSession);
+               return session;
+       }
+
+       // private synchronized Session anonymousSession(HttpServletRequest request,
+       // Repository repository, String workspace) {
+       // // TODO rather log in here as anonymous?
+       // LoginContext lc = (LoginContext)
+       // request.getAttribute(NodeConstants.LOGIN_CONTEXT_ANONYMOUS);
+       // if (lc == null)
+       // throw new CmsException("No login context available");
+       // // optimize
+       // Session session;
+       // try {
+       // session = Subject.doAs(lc.getSubject(), new
+       // PrivilegedExceptionAction<Session>() {
+       // @Override
+       // public Session run() throws Exception {
+       // return repository.login(workspace);
+       // }
+       // });
+       // } catch (Exception e) {
+       // throw new CmsException("Cannot log in to JCR", e);
+       // }
+       // return session;
+       // }
+
+       public synchronized void releaseSession(Session session) {
+               if (cmsSessions.containsKey(session)) {
+                       CmsSessionImpl cmsSession = cmsSessions.get(session);
+                       cmsSession.releaseDataSession(alias, session);
+               } else {
+                       log.warn("JCR session " + session + " not found in CMS session list. Logging it out...");
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/DataHttpContext.java b/org.argeo.cms/src/org/argeo/cms/internal/http/DataHttpContext.java
new file mode 100644 (file)
index 0000000..93f6353
--- /dev/null
@@ -0,0 +1,95 @@
+package org.argeo.cms.internal.http;
+
+import java.io.IOException;
+import java.net.URL;
+
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.auth.HttpRequestCallbackHandler;
+import org.argeo.node.NodeConstants;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.http.HttpContext;
+
+public class DataHttpContext implements HttpContext {
+       private final static Log log = LogFactory.getLog(DataHttpContext.class);
+
+       private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+       // FIXME Make it more unique
+       private final String httpAuthRealm;
+       private final boolean forceBasic;
+
+       public DataHttpContext(String httpAuthrealm, boolean forceBasic) {
+               this.httpAuthRealm = httpAuthrealm;
+               this.forceBasic = forceBasic;
+       }
+
+       public DataHttpContext(String httpAuthrealm) {
+               this(httpAuthrealm, false);
+       }
+
+       @Override
+       public boolean handleSecurity(final HttpServletRequest request, HttpServletResponse response) throws IOException {
+
+               if (log.isTraceEnabled())
+                       HttpUtils.logRequestHeaders(log, request);
+               LoginContext lc;
+               try {
+                       lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, new HttpRequestCallbackHandler(request, response));
+                       lc.login();
+               } catch (LoginException e) {
+                       lc = processUnauthorized(request, response);
+                       if (lc == null)
+                               return false;
+               }
+               return true;
+       }
+
+       @Override
+       public URL getResource(String name) {
+               return bc.getBundle().getResource(name);
+       }
+
+       @Override
+       public String getMimeType(String name) {
+               return null;
+       }
+
+       protected LoginContext processUnauthorized(HttpServletRequest request, HttpServletResponse response) {
+               // anonymous
+               try {
+                       LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS, new HttpRequestCallbackHandler(request, response));
+                       lc.login();
+                       return lc;
+               } catch (LoginException e1) {
+                       if (log.isDebugEnabled())
+                               log.error("Cannot log in as anonymous", e1);
+                       return null;
+               }
+       }
+       protected void askForWwwAuth(HttpServletRequest request, HttpServletResponse response) {
+               response.setStatus(401);
+               // response.setHeader(HttpUtils.HEADER_WWW_AUTHENTICATE, "basic
+               // realm=\"" + httpAuthRealm + "\"");
+               if (org.argeo.cms.internal.kernel.Activator.getAcceptorCredentials() != null && !forceBasic)// SPNEGO
+                       response.setHeader(HttpUtils.HEADER_WWW_AUTHENTICATE, "Negotiate");
+               else
+                       response.setHeader(HttpUtils.HEADER_WWW_AUTHENTICATE, "Basic realm=\"" + httpAuthRealm + "\"");
+
+               // response.setDateHeader("Date", System.currentTimeMillis());
+               // response.setDateHeader("Expires", System.currentTimeMillis() + (24 *
+               // 60 * 60 * 1000));
+               // response.setHeader("Accept-Ranges", "bytes");
+               // response.setHeader("Connection", "Keep-Alive");
+               // response.setHeader("Keep-Alive", "timeout=5, max=97");
+               // response.setContentType("text/html; charset=UTF-8");
+
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/HttpConstants.java b/org.argeo.cms/src/org/argeo/cms/internal/http/HttpConstants.java
new file mode 100644 (file)
index 0000000..5fe57b7
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.cms.internal.http;
+
+/** Compatible with Jetty. */
+public interface HttpConstants {
+       public static final String HTTP_ENABLED = "http.enabled";
+       public static final String HTTP_PORT = "http.port";
+       public static final String HTTP_HOST = "http.host";
+       public static final String HTTPS_ENABLED = "https.enabled";
+       public static final String HTTPS_HOST = "https.host";
+       public static final String HTTPS_PORT = "https.port";
+       public static final String SSL_KEYSTORE = "ssl.keystore";
+       public static final String SSL_PASSWORD = "ssl.password";
+       public static final String SSL_KEYPASSWORD = "ssl.keypassword";
+       public static final String SSL_NEEDCLIENTAUTH = "ssl.needclientauth";
+       public static final String SSL_WANTCLIENTAUTH = "ssl.wantclientauth";
+       public static final String SSL_PROTOCOL = "ssl.protocol";
+       public static final String SSL_ALGORITHM = "ssl.algorithm";
+       public static final String SSL_KEYSTORETYPE = "ssl.keystoretype";
+       public static final String JETTY_PROPERTY_PREFIX = "org.eclipse.equinox.http.jetty.";
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/HttpUtils.java b/org.argeo.cms/src/org/argeo/cms/internal/http/HttpUtils.java
new file mode 100644 (file)
index 0000000..58d9324
--- /dev/null
@@ -0,0 +1,64 @@
+package org.argeo.cms.internal.http;
+
+import java.util.Enumeration;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.logging.Log;
+
+public class HttpUtils {
+       public final static String HEADER_AUTHORIZATION = "Authorization";
+       public final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate";
+
+       public final static String DEFAULT_PROTECTED_HANDLERS = "/org/argeo/cms/internal/http/protectedHandlers.xml";
+       public final static String WEBDAV_CONFIG = "/org/argeo/cms/internal/http/webdav-config.xml";
+
+       static boolean isBrowser(String userAgent) {
+               return userAgent.contains("webkit") || userAgent.contains("gecko") || userAgent.contains("firefox")
+                               || userAgent.contains("msie") || userAgent.contains("chrome") || userAgent.contains("chromium")
+                               || userAgent.contains("opera") || userAgent.contains("browser");
+       }
+
+       public static void logRequestHeaders(Log log, HttpServletRequest request) {
+               if (!log.isDebugEnabled())
+                       return;
+               for (Enumeration<String> headerNames = request.getHeaderNames(); headerNames.hasMoreElements();) {
+                       String headerName = headerNames.nextElement();
+                       Object headerValue = request.getHeader(headerName);
+                       log.debug(headerName + ": " + headerValue);
+               }
+               log.debug(request.getRequestURI() + "\n");
+       }
+
+       public static void logRequest(Log log,HttpServletRequest request) {
+               log.debug("contextPath=" + request.getContextPath());
+               log.debug("servletPath=" + request.getServletPath());
+               log.debug("requestURI=" + request.getRequestURI());
+               log.debug("queryString=" + request.getQueryString());
+               StringBuilder buf = new StringBuilder();
+               // headers
+               Enumeration<String> en = request.getHeaderNames();
+               while (en.hasMoreElements()) {
+                       String header = en.nextElement();
+                       Enumeration<String> values = request.getHeaders(header);
+                       while (values.hasMoreElements())
+                               buf.append("  " + header + ": " + values.nextElement());
+                       buf.append('\n');
+               }
+
+               // attributed
+               Enumeration<String> an = request.getAttributeNames();
+               while (an.hasMoreElements()) {
+                       String attr = an.nextElement();
+                       Object value = request.getAttribute(attr);
+                       buf.append("  " + attr + ": " + value);
+                       buf.append('\n');
+               }
+               log.debug("\n" + buf);
+       }
+
+
+       private HttpUtils() {
+
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/LinkServlet.java b/org.argeo.cms/src/org/argeo/cms/internal/http/LinkServlet.java
new file mode 100644 (file)
index 0000000..34bdcaa
--- /dev/null
@@ -0,0 +1,258 @@
+package org.argeo.cms.internal.http;
+
+import static javax.jcr.Property.JCR_DESCRIPTION;
+import static javax.jcr.Property.JCR_LAST_MODIFIED;
+import static javax.jcr.Property.JCR_TITLE;
+import static org.argeo.cms.CmsTypes.CMS_IMAGE;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.PrivilegedExceptionAction;
+import java.util.Calendar;
+import java.util.Collection;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.cms.CmsException;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeUtils;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+
+public class LinkServlet extends HttpServlet {
+       private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+       private static final long serialVersionUID = 3749990143146845708L;
+
+       @Override
+       protected void service(HttpServletRequest request, HttpServletResponse response)
+                       throws ServletException, IOException {
+               String path = request.getPathInfo();
+               String userAgent = request.getHeader("User-Agent").toLowerCase();
+               boolean isBot = false;
+               // boolean isCompatibleBrowser = false;
+               if (userAgent.contains("bot") || userAgent.contains("facebook") || userAgent.contains("twitter")) {
+                       isBot = true;
+               }
+               // else if (userAgent.contains("webkit") ||
+               // userAgent.contains("gecko") || userAgent.contains("firefox")
+               // || userAgent.contains("msie") || userAgent.contains("chrome") ||
+               // userAgent.contains("chromium")
+               // || userAgent.contains("opera") || userAgent.contains("browser"))
+               // {
+               // isCompatibleBrowser = true;
+               // }
+
+               if (isBot) {
+                       // log.warn("# BOT " + request.getHeader("User-Agent"));
+                       canonicalAnswer(request, response, path);
+                       return;
+               }
+
+               // if (isCompatibleBrowser && log.isTraceEnabled())
+               // log.trace("# BWS " + request.getHeader("User-Agent"));
+               redirectTo(response, "/#" + path);
+       }
+
+       private void redirectTo(HttpServletResponse response, String location) {
+               response.setHeader("Location", location);
+               response.setStatus(HttpServletResponse.SC_FOUND);
+       }
+
+       // private boolean canonicalAnswerNeededBy(HttpServletRequest request) {
+       // String userAgent = request.getHeader("User-Agent").toLowerCase();
+       // return userAgent.startsWith("facebookexternalhit/");
+       // }
+
+       /** For bots which don't understand RWT. */
+       private void canonicalAnswer(HttpServletRequest request, HttpServletResponse response, String path) {
+               Session session = null;
+               try {
+                       PrintWriter writer = response.getWriter();
+                       session = Subject.doAs(anonymousLogin(), new PrivilegedExceptionAction<Session>() {
+
+                               @Override
+                               public Session run() throws Exception {
+                                       Collection<ServiceReference<Repository>> srs = bc.getServiceReferences(Repository.class,
+                                                       "(" + NodeConstants.CN + "=" + NodeConstants.NODE + ")");
+                                       Repository repository = bc.getService(srs.iterator().next());
+                                       return repository.login();
+                               }
+
+                       });
+                       Node node = session.getNode(path);
+                       String title = node.hasProperty(JCR_TITLE) ? node.getProperty(JCR_TITLE).getString() : node.getName();
+                       String desc = node.hasProperty(JCR_DESCRIPTION) ? node.getProperty(JCR_DESCRIPTION).getString() : null;
+                       Calendar lastUpdate = node.hasProperty(JCR_LAST_MODIFIED) ? node.getProperty(JCR_LAST_MODIFIED).getDate()
+                                       : null;
+                       String url = getCanonicalUrl(node, request);
+                       String imgUrl = null;
+                       loop: for (NodeIterator it = node.getNodes(); it.hasNext();) {
+                               // Takes the first found cms:image
+                               Node child = it.nextNode();
+                               if (child.isNodeType(CMS_IMAGE)) {
+                                       imgUrl = getDataUrl(child, request);
+                                       break loop;
+                               }
+                       }
+                       StringBuilder buf = new StringBuilder();
+                       buf.append("<html>");
+                       buf.append("<head>");
+                       writeMeta(buf, "og:title", escapeHTML(title));
+                       writeMeta(buf, "og:type", "website");
+                       buf.append("<meta name='twitter:card' content='summary' />");
+                       buf.append("<meta name='twitter:site' content='@argeo_org' />");
+                       writeMeta(buf, "og:url", url);
+                       if (desc != null)
+                               writeMeta(buf, "og:description", escapeHTML(desc));
+                       if (imgUrl != null)
+                               writeMeta(buf, "og:image", imgUrl);
+                       if (lastUpdate != null)
+                               writeMeta(buf, "og:updated_time", Long.toString(lastUpdate.getTime().getTime()));
+                       buf.append("</head>");
+                       buf.append("<body>");
+                       buf.append("<p><b>!! This page is meant for indexing robots, not for real people," + " visit <a href='/#")
+                                       .append(path).append("'>").append(escapeHTML(title)).append("</a> instead.</b></p>");
+                       writeCanonical(buf, node);
+                       buf.append("</body>");
+                       buf.append("</html>");
+                       writer.print(buf.toString());
+
+                       response.setHeader("Content-Type", "text/html");
+                       writer.flush();
+               } catch (Exception e) {
+                       throw new CmsException("Cannot write canonical answer", e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       /**
+        * From http://stackoverflow.com/questions/1265282/recommended-method-for-
+        * escaping-html-in-java (+ escaping '). TODO Use
+        * org.apache.commons.lang.StringEscapeUtils
+        */
+       private String escapeHTML(String s) {
+               StringBuilder out = new StringBuilder(Math.max(16, s.length()));
+               for (int i = 0; i < s.length(); i++) {
+                       char c = s.charAt(i);
+                       if (c > 127 || c == '\'' || c == '"' || c == '<' || c == '>' || c == '&') {
+                               out.append("&#");
+                               out.append((int) c);
+                               out.append(';');
+                       } else {
+                               out.append(c);
+                       }
+               }
+               return out.toString();
+       }
+
+       private void writeMeta(StringBuilder buf, String tag, String value) {
+               buf.append("<meta property='").append(tag).append("' content='").append(value).append("'/>");
+       }
+
+       private void writeCanonical(StringBuilder buf, Node node) throws RepositoryException {
+               buf.append("<div>");
+               if (node.hasProperty(JCR_TITLE))
+                       buf.append("<p>").append(node.getProperty(JCR_TITLE).getString()).append("</p>");
+               if (node.hasProperty(JCR_DESCRIPTION))
+                       buf.append("<p>").append(node.getProperty(JCR_DESCRIPTION).getString()).append("</p>");
+               NodeIterator children = node.getNodes();
+               while (children.hasNext()) {
+                       writeCanonical(buf, children.nextNode());
+               }
+               buf.append("</div>");
+       }
+
+       // DATA
+       private StringBuilder getServerBaseUrl(HttpServletRequest request) {
+               try {
+                       URL url = new URL(request.getRequestURL().toString());
+                       StringBuilder buf = new StringBuilder();
+                       buf.append(url.getProtocol()).append("://").append(url.getHost());
+                       if (url.getPort() != -1)
+                               buf.append(':').append(url.getPort());
+                       return buf;
+               } catch (MalformedURLException e) {
+                       throw new CmsException("Cannot extract server base URL from " + request.getRequestURL(), e);
+               }
+       }
+
+       private String getDataUrl(Node node, HttpServletRequest request) throws RepositoryException {
+               try {
+                       StringBuilder buf = getServerBaseUrl(request);
+                       buf.append(NodeUtils.getDataPath(NodeConstants.NODE, node));
+                       return new URL(buf.toString()).toString();
+               } catch (MalformedURLException e) {
+                       throw new CmsException("Cannot build data URL for " + node, e);
+               }
+       }
+
+       // public static String getDataPath(Node node) throws
+       // RepositoryException {
+       // assert node != null;
+       // String userId = node.getSession().getUserID();
+       //// if (log.isTraceEnabled())
+       //// log.trace(userId + " : " + node.getPath());
+       // StringBuilder buf = new StringBuilder();
+       // boolean isAnonymous =
+       // userId.equalsIgnoreCase(NodeConstants.ROLE_ANONYMOUS);
+       // if (isAnonymous)
+       // buf.append(WEBDAV_PUBLIC);
+       // else
+       // buf.append(WEBDAV_PRIVATE);
+       // Session session = node.getSession();
+       // Repository repository = session.getRepository();
+       // String cn;
+       // if (repository.isSingleValueDescriptor(NodeConstants.CN)) {
+       // cn = repository.getDescriptor(NodeConstants.CN);
+       // } else {
+       //// log.warn("No cn defined in repository, using " +
+       // NodeConstants.NODE);
+       // cn = NodeConstants.NODE;
+       // }
+       // return
+       // buf.append('/').append(cn).append('/').append(session.getWorkspace().getName()).append(node.getPath())
+       // .toString();
+       // }
+
+       private String getCanonicalUrl(Node node, HttpServletRequest request) throws RepositoryException {
+               try {
+                       StringBuilder buf = getServerBaseUrl(request);
+                       buf.append('/').append('!').append(node.getPath());
+                       return new URL(buf.toString()).toString();
+               } catch (MalformedURLException e) {
+                       throw new CmsException("Cannot build data URL for " + node, e);
+               }
+               // return request.getRequestURL().append('!').append(node.getPath())
+               // .toString();
+       }
+
+       private Subject anonymousLogin() {
+               Subject subject = new Subject();
+               LoginContext lc;
+               try {
+                       lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_ANONYMOUS, subject);
+                       lc.login();
+                       return subject;
+               } catch (LoginException e) {
+                       throw new CmsException("Cannot login as anonymous", e);
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/PrivateHttpContext.java b/org.argeo.cms/src/org/argeo/cms/internal/http/PrivateHttpContext.java
new file mode 100644 (file)
index 0000000..c3f2a1c
--- /dev/null
@@ -0,0 +1,24 @@
+package org.argeo.cms.internal.http;
+
+import javax.security.auth.login.LoginContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Requests authorisation */
+public class PrivateHttpContext extends DataHttpContext {
+
+       public PrivateHttpContext(String httpAuthrealm, boolean forceBasic) {
+               super(httpAuthrealm, forceBasic);
+       }
+
+       public PrivateHttpContext(String httpAuthrealm) {
+               super(httpAuthrealm);
+       }
+
+       @Override
+       protected LoginContext processUnauthorized(HttpServletRequest request, HttpServletResponse response) {
+               askForWwwAuth(request, response);
+               return null;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/RobotServlet.java b/org.argeo.cms/src/org/argeo/cms/internal/http/RobotServlet.java
new file mode 100644 (file)
index 0000000..6d3d302
--- /dev/null
@@ -0,0 +1,24 @@
+package org.argeo.cms.internal.http;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class RobotServlet extends HttpServlet {
+       private static final long serialVersionUID = 7935661175336419089L;
+
+       @Override
+       protected void service(HttpServletRequest request, HttpServletResponse response)
+                       throws ServletException, IOException {
+               PrintWriter writer = response.getWriter();
+               writer.append("User-agent: *\n");
+               writer.append("Disallow:\n");
+               response.setHeader("Content-Type", "text/plain");
+               writer.flush();
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/WebCmsSessionImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/http/WebCmsSessionImpl.java
new file mode 100644 (file)
index 0000000..1df7b17
--- /dev/null
@@ -0,0 +1,38 @@
+package org.argeo.cms.internal.http;
+
+import java.util.Locale;
+
+import javax.security.auth.Subject;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+import org.argeo.cms.internal.auth.CmsSessionImpl;
+import org.osgi.service.useradmin.Authorization;
+
+public class WebCmsSessionImpl extends CmsSessionImpl {
+       // private final static Log log =
+       // LogFactory.getLog(WebCmsSessionImpl.class);
+
+       private HttpSession httpSession;
+
+       public WebCmsSessionImpl(Subject initialSubject, Authorization authorization, Locale locale, HttpServletRequest request) {
+               super(initialSubject, authorization, locale,request.getSession(false).getId());
+               httpSession = request.getSession(false);
+       }
+
+       @Override
+       public boolean isValid() {
+               if (isClosed())
+                       return false;
+               try {// test http session
+                       httpSession.getCreationTime();
+                       return true;
+               } catch (IllegalStateException ise) {
+                       return false;
+               }
+       }
+
+       public static CmsSessionImpl getCmsSession(HttpServletRequest request) {
+               return CmsSessionImpl.getByLocalId(request.getSession(false).getId());
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/client/HttpCredentialProvider.java b/org.argeo.cms/src/org/argeo/cms/internal/http/client/HttpCredentialProvider.java
new file mode 100644 (file)
index 0000000..4a9392c
--- /dev/null
@@ -0,0 +1,20 @@
+package org.argeo.cms.internal.http.client;
+
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.auth.AuthScheme;
+import org.apache.commons.httpclient.auth.CredentialsNotAvailableException;
+import org.apache.commons.httpclient.auth.CredentialsProvider;
+
+/** SPNEGO credential provider */
+public class HttpCredentialProvider implements CredentialsProvider {
+
+       @Override
+       public Credentials getCredentials(AuthScheme scheme, String host, int port, boolean proxy)
+                       throws CredentialsNotAvailableException {
+               if (scheme instanceof SpnegoAuthScheme)
+                       return new SpnegoCredentials();
+               else
+                       throw new UnsupportedOperationException("Auth scheme " + scheme.getSchemeName() + " not supported");
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoAuthScheme.java b/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoAuthScheme.java
new file mode 100644 (file)
index 0000000..7a8071f
--- /dev/null
@@ -0,0 +1,167 @@
+package org.argeo.cms.internal.http.client;
+
+import java.net.URL;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Base64;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.URIException;
+import org.apache.commons.httpclient.auth.AuthPolicy;
+import org.apache.commons.httpclient.auth.AuthScheme;
+import org.apache.commons.httpclient.auth.AuthenticationException;
+import org.apache.commons.httpclient.auth.CredentialsProvider;
+import org.apache.commons.httpclient.auth.MalformedChallengeException;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.params.DefaultHttpParams;
+import org.apache.commons.httpclient.params.HttpParams;
+import org.argeo.cms.internal.kernel.NodeHttp;
+import org.ietf.jgss.GSSContext;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+
+/** Implementation of the SPNEGO auth scheme. */
+public class SpnegoAuthScheme implements AuthScheme {
+//     private final static Log log = LogFactory.getLog(SpnegoAuthScheme.class);
+
+       public static final String NAME = "Negotiate";
+       private final static Oid KERBEROS_OID;
+       static {
+               try {
+                       KERBEROS_OID = new Oid("1.3.6.1.5.5.2");
+               } catch (GSSException e) {
+                       throw new IllegalStateException("Cannot create Kerberos OID", e);
+               }
+       }
+
+       private boolean complete = false;
+       private String realm;
+
+       @Override
+       public void processChallenge(String challenge) throws MalformedChallengeException {
+               // if(tokenStr!=null){
+               // log.error("Received challenge while there is a token. Failing.");
+               // complete = false;
+               // }
+
+       }
+
+       @Override
+       public String getSchemeName() {
+               return NAME;
+       }
+
+       @Override
+       public String getParameter(String name) {
+               return null;
+       }
+
+       @Override
+       public String getRealm() {
+               return realm;
+       }
+
+       @Override
+       public String getID() {
+               return NAME;
+       }
+
+       @Override
+       public boolean isConnectionBased() {
+               return true;
+       }
+
+       @Override
+       public boolean isComplete() {
+               return complete;
+       }
+
+       @Override
+       public String authenticate(Credentials credentials, String method, String uri) throws AuthenticationException {
+               // log.debug("authenticate " + method + " " + uri);
+               // return null;
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public String authenticate(Credentials credentials, HttpMethod method) throws AuthenticationException {
+               GSSContext context = null;
+               String tokenStr = null;
+               String hostname;
+               try {
+                       hostname = method.getURI().getHost();
+               } catch (URIException e1) {
+                       throw new IllegalStateException("Cannot authenticate", e1);
+               }
+               String serverPrinc = NodeHttp.DEFAULT_SERVICE + "@" + hostname;
+
+               try {
+                       // Get service's principal name
+                       GSSManager manager = GSSManager.getInstance();
+                       GSSName serverName = manager.createName(serverPrinc, GSSName.NT_HOSTBASED_SERVICE, KERBEROS_OID);
+
+                       // Get the context for authentication
+                       context = manager.createContext(serverName, KERBEROS_OID, null, GSSContext.DEFAULT_LIFETIME);
+                       // context.requestMutualAuth(true); // Request mutual authentication
+                       // context.requestConf(true); // Request confidentiality
+                       context.requestCredDeleg(true);
+
+                       byte[] token = new byte[0];
+
+                       // token is ignored on the first call
+                       token = context.initSecContext(token, 0, token.length);
+
+                       // Send a token to the server if one was generated by
+                       // initSecContext
+                       if (token != null) {
+                               tokenStr = Base64.getEncoder().encodeToString(token);
+                               // complete=true;
+                       }
+                       return "Negotiate " + tokenStr;
+               } catch (GSSException e) {
+                       complete = true;
+                       throw new AuthenticationException("Cannot authenticate to " + serverPrinc, e);
+               }
+       }
+
+       public static void main(String[] args) {
+               if (args.length == 0) {
+                       System.err.println("usage: java " + SpnegoAuthScheme.class.getName() + " <url>");
+                       System.exit(1);
+                       return;
+               }
+               String url = args[0];
+
+               URL jaasUrl = SpnegoAuthScheme.class.getResource("jaas.cfg");
+               System.setProperty("java.security.auth.login.config", jaasUrl.toExternalForm());
+               try {
+                       LoginContext lc = new LoginContext("SINGLE_USER");
+                       lc.login();
+
+                       AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class);
+                       HttpParams params = DefaultHttpParams.getDefaultParams();
+                       ArrayList<String> schemes = new ArrayList<>();
+                       schemes.add(SpnegoAuthScheme.NAME);
+                       params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes);
+                       params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider());
+
+                       int responseCode = Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction<Integer>() {
+                               public Integer run() throws Exception {
+                                       HttpClient httpClient = new HttpClient();
+                                       return httpClient.executeMethod(new GetMethod(url));
+                               }
+                       });
+                       System.out.println("Reponse code: " + responseCode);
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoCredentials.java b/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoCredentials.java
new file mode 100644 (file)
index 0000000..f59b72a
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.cms.internal.http.client;
+
+import org.apache.commons.httpclient.Credentials;
+
+public class SpnegoCredentials implements Credentials {
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/client/jaas.cfg b/org.argeo.cms/src/org/argeo/cms/internal/http/client/jaas.cfg
new file mode 100644 (file)
index 0000000..21176b9
--- /dev/null
@@ -0,0 +1,5 @@
+SINGLE_USER {
+    com.sun.security.auth.module.Krb5LoginModule optional
+     principal="${user.name}"
+     useTicketCache=true;
+};
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/protectedHandlers.xml b/org.argeo.cms/src/org/argeo/cms/internal/http/protectedHandlers.xml
new file mode 100644 (file)
index 0000000..59f22cd
--- /dev/null
@@ -0,0 +1,5 @@
+<config>
+       <protecteditemremovehandler>
+               <class name="org.apache.jackrabbit.server.remoting.davex.AclRemoveHandler" />
+       </protecteditemremovehandler>
+</config>
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/webdav-config.xml b/org.argeo.cms/src/org/argeo/cms/internal/http/webdav-config.xml
new file mode 100644 (file)
index 0000000..da4e18b
--- /dev/null
@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+  -->
+<!--
+<!DOCTYPE config [
+        <!ELEMENT config (iomanager , propertymanager, (collection | noncollection)? , filter?, mimetypeproperties?) >
+
+        <!ELEMENT iomanager (class, iohandler*) >
+        <!ELEMENT iohandler (class) >
+
+        <!ELEMENT propertymanager (class, propertyhandler*) >
+        <!ELEMENT propertyhandler (class) >
+
+        <!ELEMENT collection (nodetypes) >
+        <!ELEMENT noncollection (nodetypes) >
+
+        <!ELEMENT filter (class, namespaces?, nodetypes?) >
+
+        <!ELEMENT class >
+        <!ATTLIST class
+            name  CDATA #REQUIRED
+        >
+        <!ELEMENT namespaces (prefix | uri)* >
+        <!ELEMENT prefix (CDATA) >
+        <!ELEMENT uri (CDATA) >
+
+        <!ELEMENT nodetypes (nodetype)* >
+        <!ELEMENT nodetype (CDATA) >
+
+        <!ELEMENT mimetypeproperties (mimemapping*, defaultmimetype) >
+
+        <!ELEMENT mimemapping >
+        <!ATTLIST mimemapping
+            extension  CDATA #REQUIRED
+            mimetype  CDATA #REQUIRED
+        >
+
+        <!ELEMENT defaultmimetype (CDATA) >
+]>
+-->
+
+<config>
+    <!--
+     Defines the IOManager implementation that is responsible for passing
+     import/export request to the individual IO-handlers.
+    -->
+    <iomanager>
+        <!-- class element defines the manager to be used. The specified class
+             must implement the IOManager interface.
+             Note, that the handlers are being added and called in the order
+             they appear in the configuration.
+        -->
+        <class name="org.apache.jackrabbit.server.io.IOManagerImpl" />
+        <iohandler>
+            <class name="org.apache.jackrabbit.server.io.VersionHandler" />
+        </iohandler>
+        <iohandler>
+            <class name="org.apache.jackrabbit.server.io.VersionHistoryHandler" />
+        </iohandler>
+<!--         <iohandler> -->
+<!--             <class name="org.apache.jackrabbit.server.io.ZipHandler" /> -->
+<!--         </iohandler> -->
+<!--         <iohandler> -->
+<!--             <class name="org.apache.jackrabbit.server.io.XmlHandler" /> -->
+<!--         </iohandler> -->
+        <iohandler>
+            <class name="org.apache.jackrabbit.server.io.DirListingExportHandler" />
+        </iohandler>
+        <iohandler>
+            <class name="org.apache.jackrabbit.server.io.DefaultHandler" />
+        </iohandler>
+    </iomanager>
+    <!--
+     Example config for iomanager that populates its list of handlers with
+     default values. Therefore the 'iohandler' elements are omited.
+    -->
+    <!--
+    <iomanager>
+        <class name="org.apache.jackrabbit.server.io.DefaultIOManager" />
+    </iomanager>
+    -->
+    <!--
+     Defines the PropertyManager implementation that is responsible for export
+     and import of resource properties.
+    -->
+    <propertymanager>
+        <!-- class element defines the manager to be used. The specified class
+             must implement the PropertyManager interface.
+             Note, that the handlers are being added and called in the order
+             they appear in the configuration.
+        -->
+        <class name="org.apache.jackrabbit.server.io.PropertyManagerImpl" />
+        <propertyhandler>
+            <class name="org.apache.jackrabbit.server.io.VersionHandler" />
+        </propertyhandler>
+        <propertyhandler>
+            <class name="org.apache.jackrabbit.server.io.VersionHistoryHandler" />
+        </propertyhandler>
+<!--         <propertyhandler> -->
+<!--             <class name="org.apache.jackrabbit.server.io.ZipHandler" /> -->
+<!--         </propertyhandler> -->
+<!--         <propertyhandler> -->
+<!--             <class name="org.apache.jackrabbit.server.io.XmlHandler" /> -->
+<!--         </propertyhandler> -->
+        <propertyhandler>
+            <class name="org.apache.jackrabbit.server.io.DefaultHandler" />
+        </propertyhandler>
+    </propertymanager>
+    <!--
+     Define nodetypes, that should never by displayed as 'collection'
+    -->
+    <noncollection>
+        <nodetypes>
+            <nodetype>nt:file</nodetype>
+            <nodetype>nt:resource</nodetype>
+        </nodetypes>
+    </noncollection>
+    <!--
+     Example: Defines nodetypes, that should always be displayed as 'collection'.
+    -->
+    <!--
+    <collection>
+        <nodetypes>
+            <nodetype>nt:folder</nodetype>
+            <nodetype>rep:root</nodetype>
+        </nodetypes>
+    </collection>
+    -->
+    <!--
+     Filter that allows to prevent certain items from being displayed.
+     Please note, that this has an effect on PROPFIND calls only and does not
+     provide limited access to those items matching any of the filters.
+
+     However specifying a filter may cause problems with PUT or MKCOL if the
+     resource to be created is being filtered out, thus resulting in inconsistent
+     responses (e.g. PUT followed by PROPFIND on parent).
+     -->
+    <filter>
+        <!-- class element defines the resource filter to be used. The specified class
+             must implement the ItemFilter interface -->
+        <class name="org.apache.jackrabbit.webdav.simple.DefaultItemFilter" />
+        <!--
+         Nodetype names to be used to filter child nodes.
+         A child node can be filtered if the declaring nodetype of its definition
+         is one of the nodetype names specified in the nodetypes Element.
+         E.g. defining 'rep:root' as filtered nodetype whould result in jcr:system
+         being hidden but no other child node of the root node, since those
+         are defined by the nodetype nt:unstructered.
+        -->
+        <!--
+        <nodetypes>
+            <nodetype>rep:root</nodetype>
+        </nodetypes>
+        -->
+        <!--
+         Namespace prefixes or uris. Items having a name that matches any of the
+         entries will be filtered.
+        -->
+        <namespaces>
+            <prefix>rep</prefix>
+            <prefix>jcr</prefix>
+            <!--
+            <uri>internal</uri>
+            <uri>http://www.jcp.org/jcr/1.0</uri>
+            -->
+        </namespaces>
+    </filter>
+    
+    <!--
+     Optional 'mimetypeproperties' element.
+     It defines additional or replaces existing mappings for the MimeResolver
+     instance created by the ResourceConfig.
+     The default mappings are defined in org.apache.jackrabbit.server.io.mimetypes.properties.
+     If the default mime type defined by MimeResolver is 'application/octet-stream'.
+    -->
+    <!--
+    <mimetypeproperties>
+        <mimemapping extension="rtf" mimetype="application/rtf" />
+        <mimemapping extension="ott" mimetype="application/vnd.oasis.opendocument.text-template" />
+        <defaultmimetype>text/html</defaultmimetype>
+    </mimetypeproperties>
+    -->
+</config>
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/jcr/JackrabbitType.java b/org.argeo.cms/src/org/argeo/cms/internal/jcr/JackrabbitType.java
new file mode 100644 (file)
index 0000000..cbb7930
--- /dev/null
@@ -0,0 +1,19 @@
+package org.argeo.cms.internal.jcr;
+
+/** Pre-defined Jackrabbit repository configurations. */
+enum JackrabbitType {
+       /** Local file system */
+       localfs,
+       /** Embedded Java H2 database */
+       h2,
+       /** PostgreSQL */
+       postgresql,
+       /** PostgreSQL with datastore */
+       postgresql_ds,
+       /** PostgreSQL with cluster */
+       postgresql_cluster,
+       /** PostgreSQL with cluster and datastore */
+       postgresql_cluster_ds,
+       /** Memory */
+       memory;
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/jcr/RepoConf.java b/org.argeo.cms/src/org/argeo/cms/internal/jcr/RepoConf.java
new file mode 100644 (file)
index 0000000..2601714
--- /dev/null
@@ -0,0 +1,71 @@
+package org.argeo.cms.internal.jcr;
+
+import org.argeo.osgi.metatype.EnumAD;
+import org.argeo.osgi.metatype.EnumOCD;
+
+/** JCR repository configuration */
+public enum RepoConf implements EnumAD {
+       /** Repository type */
+       type("h2"),
+       /** Default workspace */
+       defaultWorkspace("main"),
+       /** Database URL */
+       dburl(null),
+       /** Database user */
+       dbuser(null),
+       /** Database password */
+       dbpassword(null),
+
+       /** The identifier (can be an URL locating the repo) */
+       labeledUri(null),
+       //
+       // JACKRABBIT SPECIFIC
+       //
+       /** Maximum database pool size */
+       maxPoolSize(10),
+       /** Maximum cache size in MB */
+       maxCacheMB(null),
+       /** Bundle cache size in MB */
+       bundleCacheMB(8),
+       /** Extractor pool size */
+       extractorPoolSize(0),
+       /** Search cache size */
+       searchCacheSize(1000),
+       /** Max volatile index size */
+       maxVolatileIndexSize(1048576),
+       /** Cluster id (if appropriate configuration) */
+       clusterId("default"),
+       /** Indexes base path */
+       indexesBase(null);
+
+       /** The default value. */
+       private Object def;
+       private String oid;
+
+       RepoConf(String oid, Object def) {
+               this.oid = oid;
+               this.def = def;
+       }
+
+       RepoConf(Object def) {
+               this.def = def;
+       }
+
+       public Object getDefault() {
+               return def;
+       }
+
+       @Override
+       public String getID() {
+               if (oid != null)
+                       return oid;
+               return EnumAD.super.getID();
+       }
+
+       public static class OCD extends EnumOCD<RepoConf> {
+               public OCD(String locale) {
+                       super(RepoConf.class, locale);
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/jcr/RepositoryBuilder.java b/org.argeo.cms/src/org/argeo/cms/internal/jcr/RepositoryBuilder.java
new file mode 100644 (file)
index 0000000..d5f3a20
--- /dev/null
@@ -0,0 +1,213 @@
+package org.argeo.cms.internal.jcr;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Properties;
+import java.util.UUID;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.cache.CacheManager;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.apache.jackrabbit.core.config.RepositoryConfigurationParser;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.internal.kernel.CmsPaths;
+import org.argeo.jcr.ArgeoJcrException;
+import org.argeo.node.NodeConstants;
+import org.xml.sax.InputSource;
+
+/** Can interpret properties in order to create an actual JCR repository. */
+public class RepositoryBuilder {
+       private final static Log log = LogFactory.getLog(RepositoryBuilder.class);
+
+       public RepositoryContext createRepositoryContext(Dictionary<String, ?> properties) throws RepositoryException {
+               RepositoryConfig repositoryConfig = createRepositoryConfig(properties);
+               RepositoryContext repositoryContext = createJackrabbitRepository(repositoryConfig);
+               RepositoryImpl repository = repositoryContext.getRepository();
+
+               // cache
+               Object maxCacheMbStr = prop(properties, RepoConf.maxCacheMB);
+               if (maxCacheMbStr != null) {
+                       Integer maxCacheMB = Integer.parseInt(maxCacheMbStr.toString());
+                       CacheManager cacheManager = repository.getCacheManager();
+                       cacheManager.setMaxMemory(maxCacheMB * 1024l * 1024l);
+                       cacheManager.setMaxMemoryPerCache((maxCacheMB / 4) * 1024l * 1024l);
+               }
+
+               return repositoryContext;
+       }
+
+       RepositoryConfig createRepositoryConfig(Dictionary<String, ?> properties) throws RepositoryException {
+               JackrabbitType type = JackrabbitType.valueOf(prop(properties, RepoConf.type).toString());
+               ClassLoader cl = getClass().getClassLoader();
+               InputStream in = null;
+               try {
+                       final String base = "/org/argeo/cms/internal/jcr";
+                       in = cl.getResourceAsStream(base + "/repository-" + type.name() + ".xml");
+
+                       if (in == null)
+                               throw new ArgeoJcrException("Repository configuration not found");
+                       InputSource config = new InputSource(in);
+                       Properties jackrabbitVars = getConfigurationProperties(type, properties);
+                       RepositoryConfig repositoryConfig = RepositoryConfig.create(config, jackrabbitVars);
+                       return repositoryConfig;
+               } finally {
+                       IOUtils.closeQuietly(in);
+               }
+       }
+
+       private Properties getConfigurationProperties(JackrabbitType type, Dictionary<String, ?> properties) {
+               Properties props = new Properties();
+               for (Enumeration<String> keys = properties.keys(); keys.hasMoreElements();) {
+                       String key = keys.nextElement();
+                       props.put(key, properties.get(key));
+               }
+
+               // cluster id
+               // cf. https://wiki.apache.org/jackrabbit/Clustering
+               // TODO deal with multiple repos
+               String clusterId = System.getProperty("org.apache.jackrabbit.core.cluster.node_id");
+               String clusterIdProp = props.getProperty(RepoConf.clusterId.name());
+               if (clusterId != null) {
+                       if (clusterIdProp != null)
+                               throw new CmsException("Cluster id defined as System properties and in deploy config");
+                       props.put(RepoConf.clusterId.name(), clusterId);
+               } else {
+                       clusterId = clusterIdProp;
+               }
+
+               // home
+               String homeUri = props.getProperty(RepoConf.labeledUri.name());
+               Path homePath;
+               if (homeUri == null) {
+                       String cn = props.getProperty(NodeConstants.CN);
+                       assert cn != null;
+                       if (clusterId != null) {
+                               homePath = CmsPaths.getRepoDirPath(cn + '/' + clusterId);
+                       } else {
+                               homePath = CmsPaths.getRepoDirPath(cn);
+                       }
+               } else {
+                       try {
+                               URI uri = new URI(homeUri);
+                               String host = uri.getHost();
+                               if (host == null || host.trim().equals("")) {
+                                       homePath = Paths.get(uri).toAbsolutePath();
+                               } else {
+                                       // TODO remote at this stage?
+                                       throw new IllegalArgumentException("Cannot manage repository path for host " + host);
+                               }
+                       } catch (URISyntaxException e) {
+                               throw new CmsException("Invalid repository home URI", e);
+                       }
+               }
+               // TODO use Jackrabbit API (?)
+               Path rootUuidPath = homePath.resolve("repository/meta/rootUUID");
+               try {
+                       if (!Files.exists(rootUuidPath)) {
+                               Files.createDirectories(rootUuidPath.getParent());
+                               Files.write(rootUuidPath, UUID.randomUUID().toString().getBytes());
+                       }
+                       // File homeDir = homePath.toFile();
+                       // homeDir.mkdirs();
+               } catch (IOException e) {
+                       throw new CmsException("Cannot set up repository  home " + homePath, e);
+               }
+               // home cannot be overridden
+               props.put(RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE, homePath.toString());
+
+               setProp(props, RepoConf.indexesBase, CmsPaths.getRepoIndexesBase().toString());
+               // common
+               setProp(props, RepoConf.defaultWorkspace);
+               setProp(props, RepoConf.maxPoolSize);
+               // Jackrabbit defaults
+               setProp(props, RepoConf.bundleCacheMB);
+               // See http://wiki.apache.org/jackrabbit/Search
+               setProp(props, RepoConf.extractorPoolSize);
+               setProp(props, RepoConf.searchCacheSize);
+               setProp(props, RepoConf.maxVolatileIndexSize);
+
+               // specific
+               String dburl;
+               switch (type) {
+               case h2:
+                       dburl = "jdbc:h2:" + homePath.toAbsolutePath() + "/h2/repository";
+                       setProp(props, RepoConf.dburl, dburl);
+                       setProp(props, RepoConf.dbuser, "sa");
+                       setProp(props, RepoConf.dbpassword, "");
+                       break;
+               case postgresql:
+               case postgresql_ds:
+               case postgresql_cluster:
+               case postgresql_cluster_ds:
+                       dburl = "jdbc:postgresql://localhost/demo";
+                       setProp(props, RepoConf.dburl, dburl);
+                       setProp(props, RepoConf.dbuser, "argeo");
+                       setProp(props, RepoConf.dbpassword, "argeo");
+                       break;
+               case memory:
+                       break;
+               case localfs:
+                       break;
+               default:
+                       throw new ArgeoJcrException("Unsupported node type " + type);
+               }
+               return props;
+       }
+
+       private void setProp(Properties props, RepoConf key, String def) {
+               Object value = props.get(key.name());
+               if (value == null)
+                       value = def;
+               if (value == null)
+                       value = key.getDefault();
+               if (value != null)
+                       props.put(key.name(), value.toString());
+       }
+
+       private void setProp(Properties props, RepoConf key) {
+               setProp(props, key, null);
+       }
+
+       private String prop(Dictionary<String, ?> properties, RepoConf key) {
+               Object value = properties.get(key.name());
+               if (value == null)
+                       return key.getDefault() != null ? key.getDefault().toString() : null;
+               else
+                       return value.toString();
+       }
+
+       private RepositoryContext createJackrabbitRepository(RepositoryConfig repositoryConfig) throws RepositoryException {
+               ClassLoader currentContextCl = Thread.currentThread().getContextClassLoader();
+               Thread.currentThread().setContextClassLoader(RepositoryBuilder.class.getClassLoader());
+               try {
+                       long begin = System.currentTimeMillis();
+                       //
+                       // Actual repository creation
+                       //
+                       RepositoryContext repositoryContext = RepositoryContext.create(repositoryConfig);
+
+                       double duration = ((double) (System.currentTimeMillis() - begin)) / 1000;
+                       if (log.isTraceEnabled())
+                               log.trace(
+                                               "Created Jackrabbit repository in " + duration + " s, home: " + repositoryConfig.getHomeDir());
+
+                       return repositoryContext;
+               } finally {
+                       Thread.currentThread().setContextClassLoader(currentContextCl);
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-h2.xml b/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-h2.xml
new file mode 100644 (file)
index 0000000..ace0fa5
--- /dev/null
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.h2.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="h2" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="default" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="default" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+<!--                   <param name="tikaConfigPath" value="${indexesBase}/${cn}/tika-config.xml" /> -->
+                       <param name="supportHighlighting" value="true" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="default" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${indexesBase}/${cn}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+<!--           <param name="tikaConfigPath" value="${indexesBase}/${cn}/tika-config.xml" /> -->
+               <param name="supportHighlighting" value="true" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-localfs.xml b/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-localfs.xml
new file mode 100644 (file)
index 0000000..b889079
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+               <param name="path" value="${rep.home}/repository" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${wsp.home}" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${rep.home}/version" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${indexesBase}/${cn}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-memory.xml b/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-memory.xml
new file mode 100644 (file)
index 0000000..3630a14
--- /dev/null
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+                       <param name="directoryManagerClass"
+                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+                       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${indexesBase}/${cn}/index" />
+               <param name="directoryManagerClass"
+                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql.xml b/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql.xml
new file mode 100644 (file)
index 0000000..de2f245
--- /dev/null
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.postgresql.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="postgresql" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="postgresql" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${indexesBase}/${cn}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster.xml b/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster.xml
new file mode 100644 (file)
index 0000000..488ad6b
--- /dev/null
@@ -0,0 +1,87 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.postgresql.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="postgresql" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="postgresql" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${indexesBase}/${cn}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+
+       <!-- Clustering -->
+       <Cluster id="${clusterId}">
+               <Journal class="org.apache.jackrabbit.core.journal.DatabaseJournal">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="journal_" />
+               </Journal>
+       </Cluster>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster_ds.xml b/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster_ds.xml
new file mode 100644 (file)
index 0000000..ff181f1
--- /dev/null
@@ -0,0 +1,90 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.postgresql.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="postgresql" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="postgresql" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/../datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${indexesBase}/${cn}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+
+       <!-- Clustering -->
+       <Cluster id="${clusterId}">
+               <Journal class="org.apache.jackrabbit.core.journal.DatabaseJournal">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="journal_" />
+               </Journal>
+       </Cluster>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql_ds.xml b/org.argeo.cms/src/org/argeo/cms/internal/jcr/repository-postgresql_ds.xml
new file mode 100644 (file)
index 0000000..5229d16
--- /dev/null
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.postgresql.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="postgresql" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="postgresql" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${indexesBase}/${cn}/${wsp.name}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${indexesBase}/${cn}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/Activator.java
new file mode 100644 (file)
index 0000000..5ef545e
--- /dev/null
@@ -0,0 +1,207 @@
+package org.argeo.cms.internal.kernel;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Locale;
+
+import javax.security.auth.login.Configuration;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.node.ArgeoLogger;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeDeployment;
+import org.argeo.node.NodeInstance;
+import org.argeo.node.NodeState;
+import org.argeo.util.LangUtils;
+import org.ietf.jgss.GSSCredential;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogReaderService;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Activates the kernel. Gives access to kernel information for the rest of the
+ * bundle (and only it)
+ */
+public class Activator implements BundleActivator {
+       private final static Log log = LogFactory.getLog(Activator.class);
+
+       private static Activator instance;
+
+       private BundleContext bc;
+
+       private LogReaderService logReaderService;
+
+       private NodeLogger logger;
+       private CmsState nodeState;
+       private CmsDeployment nodeDeployment;
+       private CmsInstance nodeInstance;
+
+       private ServiceTracker<UserAdmin, NodeUserAdmin> userAdminSt;
+
+       @Override
+       public void start(BundleContext bundleContext) throws Exception {
+               Runtime.getRuntime().addShutdownHook(new CmsShutdown());
+               instance = this;
+               this.bc = bundleContext;
+               this.logReaderService = getService(LogReaderService.class);
+
+               try {
+                       initSecurity();
+                       initArgeoLogger();
+                       initNode();
+
+                       userAdminSt = new ServiceTracker<>(instance.bc, UserAdmin.class, null);
+                       userAdminSt.open();
+                       log.debug("Kernel bundle started");
+               } catch (Throwable e) {
+                       log.error("## FATAL: CMS activator failed", e);
+               }
+       }
+
+       private void initSecurity() {
+               if (System.getProperty(KernelConstants.JAAS_CONFIG_PROP) == null) {
+                       String jaasConfig = KernelConstants.JAAS_CONFIG;
+                       URL url = getClass().getClassLoader().getResource(jaasConfig);
+                       // System.setProperty(KernelConstants.JAAS_CONFIG_PROP,
+                       // url.toExternalForm());
+                       KernelUtils.setJaasConfiguration(url);
+               }
+               // explicitly load JAAS configuration
+               Configuration.getConfiguration();
+
+               // ConditionalPermissionAdmin permissionAdmin = bc
+               // .getService(bc.getServiceReference(ConditionalPermissionAdmin.class));
+               // ConditionalPermissionUpdate update =
+               // permissionAdmin.newConditionalPermissionUpdate();
+               // // Self
+               // update.getConditionalPermissionInfos()
+               // .add(permissionAdmin.newConditionalPermissionInfo(null,
+               // new ConditionInfo[] {
+               // new ConditionInfo(BundleLocationCondition.class.getName(), new
+               // String[] { "*" }) },
+               // new PermissionInfo[] { new
+               // PermissionInfo(AllPermission.class.getName(), null, null) },
+               // ConditionalPermissionInfo.ALLOW));
+               //
+       }
+
+       private void initArgeoLogger() {
+               logger = new NodeLogger(logReaderService);
+               bc.registerService(ArgeoLogger.class, logger, null);
+       }
+
+       private void initNode() throws IOException {
+               // Node state
+               Path stateUuidPath = bc.getDataFile("stateUuid").toPath();
+               String stateUuid;
+               if (Files.exists(stateUuidPath)) {
+                       stateUuid = Files.readAllLines(stateUuidPath).get(0);
+               } else {
+                       stateUuid = bc.getProperty(Constants.FRAMEWORK_UUID);
+                       Files.write(stateUuidPath, stateUuid.getBytes());
+               }
+               nodeState = new CmsState(stateUuid);
+               Dictionary<String, Object> regProps = LangUtils.dico(Constants.SERVICE_PID, NodeConstants.NODE_STATE_PID);
+               regProps.put(NodeConstants.CN, stateUuid);
+               bc.registerService(NodeState.class, nodeState, regProps);
+
+               // Node deployment
+               nodeDeployment = new CmsDeployment();
+               bc.registerService(NodeDeployment.class, nodeDeployment, null);
+
+               // Node instance
+               nodeInstance = new CmsInstance();
+               bc.registerService(NodeInstance.class, nodeInstance, null);
+       }
+
+       @Override
+       public void stop(BundleContext bundleContext) throws Exception {
+               try {
+                       if (nodeInstance != null)
+                               nodeInstance.shutdown();
+                       if (nodeDeployment != null)
+                               nodeDeployment.shutdown();
+                       if (nodeState != null)
+                               nodeState.shutdown();
+
+                       if (userAdminSt != null)
+                               userAdminSt.close();
+
+                       instance = null;
+                       this.bc = null;
+                       this.logReaderService = null;
+                       // this.configurationAdmin = null;
+               } catch (Exception e) {
+                       log.error("CMS activator shutdown failed", e);
+               }
+       }
+
+       private <T> T getService(Class<T> clazz) {
+               ServiceReference<T> sr = bc.getServiceReference(clazz);
+               if (sr == null)
+                       throw new CmsException("No service available for " + clazz);
+               return bc.getService(sr);
+       }
+
+       public static NodeState getNodeState() {
+               return instance.nodeState;
+       }
+
+       public static GSSCredential getAcceptorCredentials() {
+               return getNodeUserAdmin().getAcceptorCredentials();
+       }
+
+       public static boolean isSingleUser() {
+               return getNodeUserAdmin().isSingleUser();
+       }
+
+       public static UserAdmin getUserAdmin() {
+               return (UserAdmin) getNodeUserAdmin();
+       }
+
+       public static String getHttpProxySslHeader() {
+               return KernelUtils.getFrameworkProp(NodeConstants.HTTP_PROXY_SSL_DN);
+       }
+
+       private static NodeUserAdmin getNodeUserAdmin() {
+               NodeUserAdmin res;
+               try {
+                       res = instance.userAdminSt.waitForService(60000);
+               } catch (InterruptedException e) {
+                       throw new CmsException("Cannot retrieve Node user admin", e);
+               }
+               if (res == null)
+                       throw new CmsException("No Node user admin found");
+
+               return res;
+               // ServiceReference<UserAdmin> sr =
+               // instance.bc.getServiceReference(UserAdmin.class);
+               // NodeUserAdmin userAdmin = (NodeUserAdmin) instance.bc.getService(sr);
+               // return userAdmin;
+
+       }
+
+       // static CmsSecurity getCmsSecurity() {
+       // return instance.nodeSecurity;
+       // }
+
+       public String[] getLocales() {
+               // TODO optimize?
+               List<Locale> locales = getNodeState().getLocales();
+               String[] res = new String[locales.size()];
+               for (int i = 0; i < locales.size(); i++)
+                       res[i] = locales.get(i).toString();
+               return res;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsDeployment.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsDeployment.java
new file mode 100644 (file)
index 0000000..3d8a389
--- /dev/null
@@ -0,0 +1,410 @@
+package org.argeo.cms.internal.kernel;
+
+import static org.argeo.node.DataModelNamespace.CMS_DATA_MODEL_NAMESPACE;
+
+import java.io.File;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.management.ManagementFactory;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Repository;
+import javax.jcr.Session;
+import javax.security.auth.callback.CallbackHandler;
+import javax.transaction.UserTransaction;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.argeo.cms.CmsException;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.DataModelNamespace;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeDeployment;
+import org.argeo.node.NodeState;
+import org.argeo.node.security.CryptoKeyring;
+import org.argeo.node.security.Keyring;
+import org.argeo.osgi.useradmin.UserAdminConf;
+import org.argeo.util.LangUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class CmsDeployment implements NodeDeployment {
+       // private final static String LEGACY_JCR_REPOSITORY_ALIAS =
+       // "argeo.jcr.repository.alias";
+
+       private final Log log = LogFactory.getLog(getClass());
+       private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+       private DataModels dataModels;
+       private DeployConfig deployConfig;
+       private HomeRepository homeRepository;
+
+       private Long availableSince;
+
+       private final boolean cleanState;
+
+       private NodeHttp nodeHttp;
+
+       // Readiness
+       private boolean nodeAvailable = false;
+       private boolean userAdminAvailable = false;
+       private boolean httpExpected = false;
+       private boolean httpAvailable = false;
+
+       public CmsDeployment() {
+               ServiceReference<NodeState> nodeStateSr = bc.getServiceReference(NodeState.class);
+               if (nodeStateSr == null)
+                       throw new CmsException("No node state available");
+
+               NodeState nodeState = bc.getService(nodeStateSr);
+               cleanState = nodeState.isClean();
+
+               nodeHttp = new NodeHttp(cleanState);
+               dataModels = new DataModels(bc);
+               initTrackers();
+       }
+
+       private void initTrackers() {
+               ServiceTracker<?, ?> httpSt = new ServiceTracker<NodeHttp, NodeHttp>(bc, NodeHttp.class, null) {
+
+                       @Override
+                       public NodeHttp addingService(ServiceReference<NodeHttp> reference) {
+                               httpAvailable = true;
+                               checkReadiness();
+                               return super.addingService(reference);
+                       }
+               };
+               // httpSt.open();
+               KernelUtils.asyncOpen(httpSt);
+
+               ServiceTracker<?, ?> repoContextSt = new RepositoryContextStc();
+               // repoContextSt.open();
+               KernelUtils.asyncOpen(repoContextSt);
+
+               ServiceTracker<?, ?> userAdminSt = new ServiceTracker<UserAdmin, UserAdmin>(bc, UserAdmin.class, null) {
+                       @Override
+                       public UserAdmin addingService(ServiceReference<UserAdmin> reference) {
+                               UserAdmin userAdmin = super.addingService(reference);
+                               addStandardSystemRoles(userAdmin);
+                               userAdminAvailable = true;
+                               checkReadiness();
+                               return userAdmin;
+                       }
+               };
+               // userAdminSt.open();
+               KernelUtils.asyncOpen(userAdminSt);
+
+               ServiceTracker<?, ?> confAdminSt = new ServiceTracker<ConfigurationAdmin, ConfigurationAdmin>(bc,
+                               ConfigurationAdmin.class, null) {
+                       @Override
+                       public ConfigurationAdmin addingService(ServiceReference<ConfigurationAdmin> reference) {
+                               ConfigurationAdmin configurationAdmin = bc.getService(reference);
+                               deployConfig = new DeployConfig(configurationAdmin, dataModels, cleanState);
+                               httpExpected = deployConfig.getProps(KernelConstants.JETTY_FACTORY_PID, "default") != null;
+                               try {
+                                       // Configuration[] configs = configurationAdmin
+                                       // .listConfigurations("(service.factoryPid=" +
+                                       // NodeConstants.NODE_REPOS_FACTORY_PID + ")");
+                                       // for (Configuration config : configs) {
+                                       // Object cn = config.getProperties().get(NodeConstants.CN);
+                                       // if (log.isDebugEnabled())
+                                       // log.debug("Standalone repo cn: " + cn);
+                                       // }
+                                       Configuration[] configs = configurationAdmin
+                                                       .listConfigurations("(service.factoryPid=" + NodeConstants.NODE_USER_ADMIN_PID + ")");
+
+                                       boolean hasDomain = false;
+                                       for (Configuration config : configs) {
+                                               Object realm = config.getProperties().get(UserAdminConf.realm.name());
+                                               if (realm != null) {
+                                                       log.debug("Found realm: " + realm);
+                                                       hasDomain = true;
+                                               }
+                                       }
+                                       if (hasDomain) {
+                                               loadIpaJaasConfiguration();
+                                       }
+                               } catch (Exception e) {
+                                       throw new CmsException("Cannot initialize config", e);
+                               }
+                               return super.addingService(reference);
+                       }
+               };
+               // confAdminSt.open();
+               KernelUtils.asyncOpen(confAdminSt);
+       }
+
+       private void addStandardSystemRoles(UserAdmin userAdmin) {
+               // we assume UserTransaction is already available (TODO make it more robust)
+               UserTransaction userTransaction = bc.getService(bc.getServiceReference(UserTransaction.class));
+               try {
+                       userTransaction.begin();
+                       Role adminRole = userAdmin.getRole(NodeConstants.ROLE_ADMIN);
+                       if (adminRole == null) {
+                               adminRole = userAdmin.createRole(NodeConstants.ROLE_ADMIN, Role.GROUP);
+                       }
+                       if (userAdmin.getRole(NodeConstants.ROLE_USER_ADMIN) == null) {
+                               Group userAdminRole = (Group) userAdmin.createRole(NodeConstants.ROLE_USER_ADMIN, Role.GROUP);
+                               userAdminRole.addMember(adminRole);
+                       }
+                       userTransaction.commit();
+               } catch (Exception e) {
+                       try {
+                               userTransaction.rollback();
+                       } catch (Exception e1) {
+                               // silent
+                       }
+                       throw new CmsException("Cannot add standard system roles", e);
+               }
+       }
+
+       private void loadIpaJaasConfiguration() {
+               if (System.getProperty(KernelConstants.JAAS_CONFIG_PROP) == null) {
+                       String jaasConfig = KernelConstants.JAAS_CONFIG_IPA;
+                       URL url = getClass().getClassLoader().getResource(jaasConfig);
+                       KernelUtils.setJaasConfiguration(url);
+                       log.debug("Set IPA JAAS configuration.");
+               }
+       }
+
+       public void shutdown() {
+               if (nodeHttp != null)
+                       nodeHttp.destroy();
+               if (deployConfig != null) {
+                       new Thread(() -> deployConfig.save(), "Save Argeo Deploy Config").start();
+               }
+       }
+
+       private void checkReadiness() {
+               if (nodeAvailable && userAdminAvailable && (httpExpected ? httpAvailable : true)) {
+                       String data = KernelUtils.getFrameworkProp(KernelUtils.OSGI_INSTANCE_AREA);
+                       String state = KernelUtils.getFrameworkProp(KernelUtils.OSGI_CONFIGURATION_AREA);
+                       availableSince = System.currentTimeMillis();
+                       long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
+                       String jvmUptimeStr = " in " + (jvmUptime / 1000) + "." + (jvmUptime % 1000) + "s";
+                       log.info("## ARGEO NODE AVAILABLE" + (log.isDebugEnabled() ? jvmUptimeStr : "") + " ##");
+                       if (log.isDebugEnabled()) {
+                               log.debug("## state: " + state);
+                               if (data != null)
+                                       log.debug("## data: " + data);
+                       }
+                       long begin = bc.getService(bc.getServiceReference(NodeState.class)).getAvailableSince();
+                       long initDuration = System.currentTimeMillis() - begin;
+                       if (log.isTraceEnabled())
+                               log.trace("Kernel initialization took " + initDuration + "ms");
+                       tributeToFreeSoftware(initDuration);
+               }
+       }
+
+       final private void tributeToFreeSoftware(long initDuration) {
+               if (log.isTraceEnabled()) {
+                       long ms = initDuration / 100;
+                       log.trace("Spend " + ms + "ms" + " reflecting on the progress brought to mankind" + " by Free Software...");
+                       long beginNano = System.nanoTime();
+                       try {
+                               Thread.sleep(ms, 0);
+                       } catch (InterruptedException e) {
+                               // silent
+                       }
+                       long durationNano = System.nanoTime() - beginNano;
+                       final double M = 1000d * 1000d;
+                       double sleepAccuracy = ((double) durationNano) / (ms * M);
+                       log.trace("Sleep accuracy: " + String.format("%.2f", 100 - (sleepAccuracy * 100 - 100)) + " %");
+               }
+       }
+
+       private void prepareNodeRepository(Repository deployedNodeRepository) {
+               if (availableSince != null) {
+                       throw new CmsException("Deployment is already available");
+               }
+
+               // home
+               prepareDataModel(NodeConstants.NODE, KernelUtils.openAdminSession(deployedNodeRepository));
+       }
+
+       private void prepareHomeRepository(RepositoryImpl deployedRepository) {
+               Hashtable<String, String> regProps = new Hashtable<String, String>();
+               regProps.put(NodeConstants.CN, NodeConstants.HOME);
+               // regProps.put(LEGACY_JCR_REPOSITORY_ALIAS, NodeConstants.HOME);
+               homeRepository = new HomeRepository(deployedRepository, false);
+               // register
+               bc.registerService(Repository.class, homeRepository, regProps);
+
+               new ServiceTracker<CallbackHandler, CallbackHandler>(bc, CallbackHandler.class, null) {
+
+                       @Override
+                       public CallbackHandler addingService(ServiceReference<CallbackHandler> reference) {
+                               NodeKeyRing nodeKeyring = new NodeKeyRing(homeRepository);
+                               CallbackHandler callbackHandler = bc.getService(reference);
+                               nodeKeyring.setDefaultCallbackHandler(callbackHandler);
+                               bc.registerService(LangUtils.names(Keyring.class, CryptoKeyring.class, ManagedService.class),
+                                               nodeKeyring, LangUtils.dico(Constants.SERVICE_PID, NodeConstants.NODE_KEYRING_PID));
+                               return callbackHandler;
+                       }
+
+               }.open();
+       }
+
+       /** Session is logged out. */
+       private void prepareDataModel(String cn, Session adminSession) {
+               try {
+                       Set<String> processed = new HashSet<String>();
+                       bundles: for (Bundle bundle : bc.getBundles()) {
+                               BundleWiring wiring = bundle.adapt(BundleWiring.class);
+                               if (wiring == null)
+                                       continue bundles;
+                               if (NodeConstants.NODE.equals(cn))// process all data models
+                                       processWiring(cn, adminSession, wiring, processed);
+                               else {
+                                       List<BundleCapability> capabilities = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE);
+                                       for (BundleCapability capability : capabilities) {
+                                               String dataModelName = (String) capability.getAttributes().get(DataModelNamespace.NAME);
+                                               if (dataModelName.equals(cn))// process only own data model
+                                                       processWiring(cn, adminSession, wiring, processed);
+                                       }
+                               }
+                       }
+               } finally {
+                       JcrUtils.logoutQuietly(adminSession);
+               }
+       }
+
+       private void processWiring(String cn, Session adminSession, BundleWiring wiring, Set<String> processed) {
+               // recursively process requirements first
+               List<BundleWire> requiredWires = wiring.getRequiredWires(CMS_DATA_MODEL_NAMESPACE);
+               for (BundleWire wire : requiredWires) {
+                       processWiring(cn, adminSession, wire.getProviderWiring(), processed);
+               }
+
+               List<String> publishAsLocalRepo = new ArrayList<>();
+               List<BundleCapability> capabilities = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE);
+               for (BundleCapability capability : capabilities) {
+                       boolean publish = registerDataModelCapability(cn, adminSession, capability, processed);
+                       if (publish)
+                               publishAsLocalRepo.add((String) capability.getAttributes().get(DataModelNamespace.NAME));
+               }
+               // Publish all at once, so that bundles with multiple CNDs are consistent
+               for (String dataModelName : publishAsLocalRepo)
+                       publishLocalRepo(dataModelName, adminSession.getRepository());
+       }
+
+       private boolean registerDataModelCapability(String cn, Session adminSession, BundleCapability capability,
+                       Set<String> processed) {
+               Map<String, Object> attrs = capability.getAttributes();
+               String name = (String) attrs.get(DataModelNamespace.NAME);
+               if (processed.contains(name)) {
+                       if (log.isTraceEnabled())
+                               log.trace("Data model " + name + " has already been processed");
+                       return false;
+               }
+
+               // CND
+               String path = (String) attrs.get(DataModelNamespace.CND);
+               if (path != null) {
+                       File dataModel = bc.getBundle().getDataFile("dataModels/" + path);
+                       if (!dataModel.exists()) {
+                               URL url = capability.getRevision().getBundle().getResource(path);
+                               if (url == null)
+                                       throw new CmsException("No data model '" + name + "' found under path " + path);
+                               try (Reader reader = new InputStreamReader(url.openStream())) {
+                                       CndImporter.registerNodeTypes(reader, adminSession, true);
+                                       processed.add(name);
+                                       dataModel.getParentFile().mkdirs();
+                                       dataModel.createNewFile();
+                                       if (log.isDebugEnabled())
+                                               log.debug("Registered CND " + url);
+                               } catch (Exception e) {
+                                       throw new CmsException("Cannot import CND " + url, e);
+                               }
+                       }
+               }
+
+               if (KernelUtils.asBoolean((String) attrs.get(DataModelNamespace.ABSTRACT)))
+                       return false;
+               // Non abstract
+               boolean isStandalone = deployConfig.isStandalone(name);
+               boolean publishLocalRepo;
+               if (isStandalone && name.equals(cn))// includes the node itself
+                       publishLocalRepo = true;
+               else if (!isStandalone && cn.equals(NodeConstants.NODE))
+                       publishLocalRepo = true;
+               else
+                       publishLocalRepo = false;
+
+               return publishLocalRepo;
+       }
+
+       private void publishLocalRepo(String dataModelName, Repository repository) {
+               Hashtable<String, Object> properties = new Hashtable<>();
+               // properties.put(LEGACY_JCR_REPOSITORY_ALIAS, name);
+               properties.put(NodeConstants.CN, dataModelName);
+               if (dataModelName.equals(NodeConstants.NODE))
+                       properties.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
+               LocalRepository localRepository = new LocalRepository(repository, dataModelName);
+               bc.registerService(Repository.class, localRepository, properties);
+               if (log.isDebugEnabled())
+                       log.debug("Published data model " + dataModelName);
+       }
+
+       @Override
+       public Long getAvailableSince() {
+               return availableSince;
+       }
+
+       private class RepositoryContextStc extends ServiceTracker<RepositoryContext, RepositoryContext> {
+
+               public RepositoryContextStc() {
+                       super(bc, RepositoryContext.class, null);
+               }
+
+               @Override
+               public RepositoryContext addingService(ServiceReference<RepositoryContext> reference) {
+                       RepositoryContext repoContext = bc.getService(reference);
+                       String cn = (String) reference.getProperty(NodeConstants.CN);
+                       if (cn != null) {
+                               if (cn.equals(NodeConstants.NODE)) {
+                                       prepareNodeRepository(repoContext.getRepository());
+                                       // TODO separate home repository
+                                       prepareHomeRepository(repoContext.getRepository());
+                                       nodeAvailable = true;
+                                       checkReadiness();
+                               } else {
+                                       prepareDataModel(cn, KernelUtils.openAdminSession(repoContext.getRepository()));
+                               }
+                       }
+                       return repoContext;
+               }
+
+               @Override
+               public void modifiedService(ServiceReference<RepositoryContext> reference, RepositoryContext service) {
+               }
+
+               @Override
+               public void removedService(ServiceReference<RepositoryContext> reference, RepositoryContext service) {
+               }
+
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsFsProvider.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsFsProvider.java
new file mode 100644 (file)
index 0000000..614ff6c
--- /dev/null
@@ -0,0 +1,95 @@
+package org.argeo.cms.internal.kernel;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.jackrabbit.fs.AbstractJackrabbitFsProvider;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.jcr.fs.JcrFileSystem;
+import org.argeo.jcr.fs.JcrFsException;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeUtils;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+public class CmsFsProvider extends AbstractJackrabbitFsProvider {
+       private Map<String, JcrFileSystem> fileSystems = new HashMap<>();
+       private BundleContext bc = FrameworkUtil.getBundle(CmsFsProvider.class).getBundleContext();
+
+       @Override
+       public String getScheme() {
+               return NodeConstants.SCHEME_NODE;
+       }
+
+       @Override
+       public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+               String username = CurrentUser.getUsername();
+               if (username == null) {
+                       // TODO deal with anonymous
+                       return null;
+               }
+               if (fileSystems.containsKey(username))
+                       throw new FileSystemAlreadyExistsException("CMS file system already exists for user " + username);
+
+               try {
+                       String host = uri.getHost();
+                       if (host != null && !host.trim().equals("")) {
+                               URI repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), "/jcr/node", null, null);
+                               RepositoryFactory repositoryFactory = bc.getService(bc.getServiceReference(RepositoryFactory.class));
+                               Repository repository = NodeUtils.getRepositoryByUri(repositoryFactory, repoUri.toString());
+                               Session session = repository.login("main");
+                               JcrFileSystem fileSystem = new JcrFileSystem(this, session);
+                               fileSystems.put(username, fileSystem);
+                               return fileSystem;
+                       } else {
+                               Repository repository = bc.getService(
+                                               bc.getServiceReferences(Repository.class, "(cn=" + NodeConstants.HOME + ")").iterator().next());
+                               Session session = repository.login();
+                               JcrFileSystem fileSystem = new JcrFileSystem(this, session);
+                               fileSystems.put(username, fileSystem);
+                               return fileSystem;
+                       }
+               } catch (Exception e) {
+                       throw new CmsException("Cannot open file system " + uri + " for user " + username, e);
+               }
+       }
+
+       @Override
+       public FileSystem getFileSystem(URI uri) {
+               return currentUserFileSystem();
+       }
+
+       @Override
+       public Path getPath(URI uri) {
+               JcrFileSystem fileSystem = currentUserFileSystem();
+               String path = uri.getPath();
+               if (fileSystem == null)
+                       try {
+                               fileSystem = (JcrFileSystem) newFileSystem(uri, new HashMap<String, Object>());
+                       } catch (IOException e) {
+                               throw new JcrFsException("Could not autocreate file system", e);
+                       }
+               return fileSystem.getPath(path);
+       }
+
+       protected JcrFileSystem currentUserFileSystem() {
+               String username = CurrentUser.getUsername();
+               return fileSystems.get(username);
+       }
+
+       public Node getUserHome(Session session) {
+               return NodeUtils.getUserHome(session);
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsInstance.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsInstance.java
new file mode 100644 (file)
index 0000000..d040bdb
--- /dev/null
@@ -0,0 +1,61 @@
+package org.argeo.cms.internal.kernel;
+
+import javax.jcr.Repository;
+import javax.naming.ldap.LdapName;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeInstance;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+public class CmsInstance implements NodeInstance {
+       private final Log log = LogFactory.getLog(getClass());
+       private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+       private HomeRepository homeRepository;
+
+       public CmsInstance() {
+               initTrackers();
+       }
+
+       private void initTrackers() {
+               // node repository
+               new ServiceTracker<Repository, Repository>(bc, Repository.class, null) {
+                       @Override
+                       public Repository addingService(ServiceReference<Repository> reference) {
+                               Object cn = reference.getProperty(NodeConstants.CN);
+                               if (cn != null && cn.equals(NodeConstants.HOME)) {
+                                       homeRepository = (HomeRepository) bc.getService(reference);
+                                       if (log.isDebugEnabled())
+                                               log.debug("Home repository is available");
+                               }
+                               return super.addingService(reference);
+                       }
+
+                       @Override
+                       public void removedService(ServiceReference<Repository> reference, Repository service) {
+                               super.removedService(reference, service);
+                               homeRepository = null;
+                       }
+
+               }.open();
+       }
+
+       public void shutdown() {
+
+       }
+
+       @Override
+       public void createWorkgroup(LdapName dn) {
+               if (homeRepository == null)
+                       throw new CmsException("Home repository is not available");
+               // TODO add check that the group exists
+               homeRepository.createWorkgroup(dn);
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsPaths.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsPaths.java
new file mode 100644 (file)
index 0000000..452edc9
--- /dev/null
@@ -0,0 +1,17 @@
+package org.argeo.cms.internal.kernel;
+
+import java.nio.file.Path;
+
+public class CmsPaths {
+       public static Path getRepoDirPath(String cn) {
+               return KernelUtils.getOsgiInstancePath(KernelConstants.DIR_REPOS + '/' + cn);
+       }
+
+       public static Path getRepoIndexesBase() {
+               return KernelUtils.getOsgiInstancePath(KernelConstants.DIR_INDEXES);
+       }
+
+       private CmsPaths() {
+
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsShutdown.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsShutdown.java
new file mode 100644 (file)
index 0000000..a62ee7f
--- /dev/null
@@ -0,0 +1,70 @@
+package org.argeo.cms.internal.kernel;
+
+import java.io.IOException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.launch.Framework;
+
+/** Shutdowns the OSGi framework */
+class CmsShutdown extends Thread {
+       public final int EXIT_OK = 0;
+       public final int EXIT_ERROR = 1;
+       public final int EXIT_TIMEOUT = 2;
+       public final int EXIT_UNKNOWN = 3;
+
+       private final Log log = LogFactory.getLog(CmsShutdown.class);
+       // private final BundleContext bc =
+       // FrameworkUtil.getBundle(CmsShutdown.class).getBundleContext();
+       private final Framework framework;
+
+       /** Shutdown timeout in ms */
+       private long timeout = 10 * 60 * 1000;
+
+       public CmsShutdown() {
+               super("CMS Shutdown Hook");
+               framework = (Framework) FrameworkUtil.getBundle(CmsShutdown.class).getBundleContext().getBundle(0);
+       }
+
+       @Override
+       public void run() {
+               if (framework.getState() != Bundle.ACTIVE) {
+                       return;
+               }
+               
+               if (log.isDebugEnabled())
+                       log.debug("Shutting down OSGi framework...");
+               try {
+                       // shutdown framework
+                       framework.stop();
+                       // wait for shutdown
+                       FrameworkEvent shutdownEvent = framework.waitForStop(timeout);
+                       int stoppedType = shutdownEvent.getType();
+                       Runtime runtime = Runtime.getRuntime();
+                       if (stoppedType == FrameworkEvent.STOPPED) {
+                               // close VM
+                               //System.exit(EXIT_OK);
+                       } else if (stoppedType == FrameworkEvent.ERROR) {
+                               log.error("The OSGi framework stopped with an error");
+                               runtime.halt(EXIT_ERROR);
+                       } else if (stoppedType == FrameworkEvent.WAIT_TIMEDOUT) {
+                               log.error("The OSGi framework hasn't stopped after " + timeout + "ms."
+                                               + " Forcibly terminating the JVM...");
+                               runtime.halt(EXIT_TIMEOUT);
+                       } else {
+                               log.error("Unknown state of OSGi framework after " + timeout + "ms."
+                                               + " Forcibly terminating the JVM... (" + shutdownEvent + ")");
+                               runtime.halt(EXIT_UNKNOWN);
+                       }
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       log.error("Unexpected exception " + e + " in shutdown hook. " + " Forcibly terminating the JVM...");
+                       Runtime.getRuntime().halt(EXIT_UNKNOWN);
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsState.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/CmsState.java
new file mode 100644 (file)
index 0000000..e3d48a6
--- /dev/null
@@ -0,0 +1,228 @@
+package org.argeo.cms.internal.kernel;
+
+import static bitronix.tm.TransactionManagerServices.getTransactionManager;
+import static bitronix.tm.TransactionManagerServices.getTransactionSynchronizationRegistry;
+import static java.util.Locale.ENGLISH;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+import javax.jcr.RepositoryFactory;
+import javax.transaction.TransactionManager;
+import javax.transaction.TransactionSynchronizationRegistry;
+import javax.transaction.UserTransaction;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.i18n.LocaleUtils;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeState;
+import org.argeo.transaction.simple.SimpleTransactionManager;
+import org.argeo.util.LangUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.ManagedServiceFactory;
+
+import bitronix.tm.BitronixTransactionManager;
+import bitronix.tm.BitronixTransactionSynchronizationRegistry;
+import bitronix.tm.TransactionManagerServices;
+
+public class CmsState implements NodeState {
+       private final static Log log = LogFactory.getLog(CmsState.class);
+       private final BundleContext bc = FrameworkUtil.getBundle(CmsState.class).getBundleContext();
+
+       // REFERENCES
+       private Long availableSince;
+
+       // i18n
+       private Locale defaultLocale;
+       private List<Locale> locales = null;
+
+       private ThreadGroup threadGroup = new ThreadGroup("CMS");
+       private KernelThread kernelThread;
+       private List<Runnable> stopHooks = new ArrayList<>();
+
+       private final String stateUuid;
+       private final boolean cleanState;
+       private String hostname;
+
+       public CmsState(String stateUuid) {
+               this.stateUuid = stateUuid;
+               String frameworkUuid = KernelUtils.getFrameworkProp(Constants.FRAMEWORK_UUID);
+               this.cleanState = stateUuid.equals(frameworkUuid);
+               try {
+                       this.hostname = InetAddress.getLocalHost().getHostName();
+               } catch (UnknownHostException e) {
+                       log.error("Cannot set hostname: " + e);
+               }
+
+               availableSince = System.currentTimeMillis();
+               if (log.isDebugEnabled())
+                       log.debug("## CMS starting... stateUuid=" + this.stateUuid + (cleanState ? " (clean state) " : " "));
+
+               initI18n();
+               initServices();
+
+               // kernel thread
+               kernelThread = new KernelThread(threadGroup, "Kernel Thread");
+               kernelThread.setContextClassLoader(getClass().getClassLoader());
+               kernelThread.start();
+       }
+
+       private void initI18n() {
+               Object defaultLocaleValue = KernelUtils.getFrameworkProp(NodeConstants.I18N_DEFAULT_LOCALE);
+               defaultLocale = defaultLocaleValue != null ? new Locale(defaultLocaleValue.toString())
+                               : new Locale(ENGLISH.getLanguage());
+               locales = LocaleUtils.asLocaleList(KernelUtils.getFrameworkProp(NodeConstants.I18N_LOCALES));
+       }
+
+       private void initServices() {
+               // JTA
+               String tmType = KernelUtils.getFrameworkProp(NodeConstants.TRANSACTION_MANAGER,
+                               NodeConstants.TRANSACTION_MANAGER_SIMPLE);
+               if (NodeConstants.TRANSACTION_MANAGER_SIMPLE.equals(tmType)) {
+                       initSimpleTransactionManager();
+               } else if (NodeConstants.TRANSACTION_MANAGER_BITRONIX.equals(tmType)) {
+                       initBitronixTransactionManager();
+               } else {
+                       throw new CmsException("Usupported transaction manager type " + tmType);
+               }
+
+               
+               // POI
+//             POIXMLTypeLoader.setClassLoader(CTConnection.class.getClassLoader());
+               
+               // Tika
+//             OpenDocumentParser odfParser = new OpenDocumentParser();
+//             bc.registerService(Parser.class, odfParser, new Hashtable());
+//             PDFParser pdfParser = new PDFParser();
+//             bc.registerService(Parser.class, pdfParser, new Hashtable());
+//             OOXMLParser ooxmlParser = new OOXMLParser();
+//             bc.registerService(Parser.class, ooxmlParser, new Hashtable());
+//             TesseractOCRParser ocrParser = new TesseractOCRParser();
+//             ocrParser.setLanguage("ara");
+//             bc.registerService(Parser.class, ocrParser, new Hashtable());
+
+               // JCR
+               RepositoryServiceFactory repositoryServiceFactory = new RepositoryServiceFactory();
+               stopHooks.add(() -> repositoryServiceFactory.shutdown());
+               bc.registerService(ManagedServiceFactory.class, repositoryServiceFactory,
+                               LangUtils.dico(Constants.SERVICE_PID, NodeConstants.NODE_REPOS_FACTORY_PID));
+
+               NodeRepositoryFactory repositoryFactory = new NodeRepositoryFactory();
+               bc.registerService(RepositoryFactory.class, repositoryFactory, null);
+
+               // Security
+               NodeUserAdmin userAdmin = new NodeUserAdmin(NodeConstants.ROLES_BASEDN);
+               stopHooks.add(() -> userAdmin.destroy());
+               bc.registerService(ManagedServiceFactory.class, userAdmin,
+                               LangUtils.dico(Constants.SERVICE_PID, NodeConstants.NODE_USER_ADMIN_PID));
+
+               // File System
+               CmsFsProvider cmsFsProvider = new CmsFsProvider();
+               bc.registerService(FileSystemProvider.class, cmsFsProvider,
+                               LangUtils.dico(Constants.SERVICE_PID, NodeConstants.NODE_FS_PROVIDER_PID));
+       }
+
+       private void initSimpleTransactionManager() {
+               SimpleTransactionManager transactionManager = new SimpleTransactionManager();
+               bc.registerService(TransactionManager.class, transactionManager, null);
+               bc.registerService(UserTransaction.class, transactionManager, null);
+               // TODO TransactionSynchronizationRegistry
+       }
+
+       private void initBitronixTransactionManager() {
+               // TODO manage it in a managed service, as startup could be long
+               ServiceReference<TransactionManager> existingTm = bc.getServiceReference(TransactionManager.class);
+               if (existingTm != null) {
+                       if (log.isDebugEnabled())
+                               log.debug("Using provided transaction manager " + existingTm);
+                       return;
+               }
+
+               if (!TransactionManagerServices.isTransactionManagerRunning()) {
+                       bitronix.tm.Configuration tmConf = TransactionManagerServices.getConfiguration();
+                       tmConf.setServerId(UUID.randomUUID().toString());
+
+                       Bundle bitronixBundle = FrameworkUtil.getBundle(bitronix.tm.Configuration.class);
+                       File tmBaseDir = bitronixBundle.getDataFile(KernelConstants.DIR_TRANSACTIONS);
+                       File tmDir1 = new File(tmBaseDir, "btm1");
+                       tmDir1.mkdirs();
+                       tmConf.setLogPart1Filename(new File(tmDir1, tmDir1.getName() + ".tlog").getAbsolutePath());
+                       File tmDir2 = new File(tmBaseDir, "btm2");
+                       tmDir2.mkdirs();
+                       tmConf.setLogPart2Filename(new File(tmDir2, tmDir2.getName() + ".tlog").getAbsolutePath());
+               }
+               BitronixTransactionManager transactionManager = getTransactionManager();
+               stopHooks.add(() -> transactionManager.shutdown());
+               BitronixTransactionSynchronizationRegistry transactionSynchronizationRegistry = getTransactionSynchronizationRegistry();
+               // register
+               bc.registerService(TransactionManager.class, transactionManager, null);
+               bc.registerService(UserTransaction.class, transactionManager, null);
+               bc.registerService(TransactionSynchronizationRegistry.class, transactionSynchronizationRegistry, null);
+               if (log.isDebugEnabled())
+                       log.debug("Initialised default Bitronix transaction manager");
+       }
+
+       void shutdown() {
+               if (log.isDebugEnabled())
+                       log.debug("CMS stopping...  stateUuid=" + this.stateUuid + (cleanState ? " (clean state) " : " "));
+
+               if (kernelThread != null)
+                       kernelThread.destroyAndJoin();
+               // In a different state in order to avois interruptions
+               new Thread(() -> applyStopHooks(), "Apply Argeo Stop Hooks").start();
+               // applyStopHooks();
+
+               long duration = ((System.currentTimeMillis() - availableSince) / 1000) / 60;
+               log.info("## ARGEO CMS STOPPED after " + (duration / 60) + "h " + (duration % 60) + "min uptime ##");
+       }
+
+       /** Apply shutdown hoos in reverse order. */
+       private void applyStopHooks() {
+               for (int i = stopHooks.size() - 1; i >= 0; i--) {
+                       try {
+                               stopHooks.get(i).run();
+                       } catch (Exception e) {
+                               log.error("Could not run shutdown hook #" + i);
+                       }
+               }
+               // Clean hanging Gogo shell thread
+               new GogoShellKiller().start();
+       }
+
+       @Override
+       public boolean isClean() {
+               return cleanState;
+       }
+
+       @Override
+       public Long getAvailableSince() {
+               return availableSince;
+       }
+
+       /*
+        * ACCESSORS
+        */
+       public Locale getDefaultLocale() {
+               return defaultLocale;
+       }
+
+       public List<Locale> getLocales() {
+               return locales;
+       }
+
+       public String getHostname() {
+               return hostname;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/DataModels.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/DataModels.java
new file mode 100644 (file)
index 0000000..da63281
--- /dev/null
@@ -0,0 +1,179 @@
+package org.argeo.cms.internal.kernel;
+
+import static org.argeo.node.DataModelNamespace.CMS_DATA_MODEL_NAMESPACE;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.node.DataModelNamespace;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleListener;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+
+class DataModels implements BundleListener {
+       private final static Log log = LogFactory.getLog(DataModels.class);
+
+       private Map<String, DataModel> dataModels = new TreeMap<>();
+
+       public DataModels(BundleContext bc) {
+               for (Bundle bundle : bc.getBundles())
+                       processBundle(bundle);
+               bc.addBundleListener(this);
+       }
+
+       public List<DataModel> getNonAbstractDataModels() {
+               List<DataModel> res = new ArrayList<>();
+               for (String name : dataModels.keySet()) {
+                       DataModel dataModel = dataModels.get(name);
+                       if (!dataModel.isAbstract())
+                               res.add(dataModel);
+               }
+               // TODO reorder?
+               return res;
+       }
+
+       @Override
+       public void bundleChanged(BundleEvent event) {
+               if (event.getType() == Bundle.RESOLVED) {
+                       processBundle(event.getBundle());
+               } else if (event.getType() == Bundle.UNINSTALLED) {
+                       BundleWiring wiring = event.getBundle().adapt(BundleWiring.class);
+                       List<BundleCapability> providedDataModels = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE);
+                       if (providedDataModels.size() == 0)
+                               return;
+                       for (BundleCapability bundleCapability : providedDataModels) {
+                               dataModels.remove(bundleCapability.getAttributes().get(DataModelNamespace.NAME));
+                       }
+               }
+
+       }
+
+       protected void processBundle(Bundle bundle) {
+               BundleWiring wiring = bundle.adapt(BundleWiring.class);
+               if (wiring == null) {
+                       log.warn("Bundle " + bundle.getSymbolicName() + " #" + bundle.getBundleId() + " (" + bundle.getLocation()
+                                       + ") cannot be adapted to a wiring");
+                       return;
+               }
+               List<BundleCapability> providedDataModels = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE);
+               if (providedDataModels.size() == 0)
+                       return;
+               List<BundleWire> requiredDataModels = wiring.getRequiredWires(CMS_DATA_MODEL_NAMESPACE);
+               // process requirements first
+               for (BundleWire bundleWire : requiredDataModels) {
+                       processBundle(bundleWire.getProvider().getBundle());
+               }
+               for (BundleCapability bundleCapability : providedDataModels) {
+                       String name = (String) bundleCapability.getAttributes().get(DataModelNamespace.NAME);
+                       assert name != null;
+                       if (!dataModels.containsKey(name)) {
+                               DataModel dataModel = new DataModel(name, bundleCapability, requiredDataModels);
+                               dataModels.put(dataModel.getName(), dataModel);
+                       }
+               }
+       }
+
+       /** Return a negative depth if dataModel is required by ref, 0 otherwise. */
+       static int required(DataModel ref, DataModel dataModel, int depth) {
+               for (DataModel dm : ref.getRequired()) {
+                       if (dm.equals(dataModel))// found here
+                               return depth - 1;
+                       int d = required(dm, dataModel, depth - 1);
+                       if (d != 0)// found deeper
+                               return d;
+               }
+               return 0;// not found
+       }
+
+       class DataModel {
+               private final String name;
+               private final boolean abstrct;
+               // private final boolean standalone;
+               private final String cnd;
+               private final List<DataModel> required;
+
+               private DataModel(String name, BundleCapability bundleCapability, List<BundleWire> requiredDataModels) {
+                       assert CMS_DATA_MODEL_NAMESPACE.equals(bundleCapability.getNamespace());
+                       this.name = name;
+                       Map<String, Object> attrs = bundleCapability.getAttributes();
+                       abstrct = KernelUtils.asBoolean((String) attrs.get(DataModelNamespace.ABSTRACT));
+                       // standalone = KernelUtils.asBoolean((String)
+                       // attrs.get(DataModelNamespace.CAPABILITY_STANDALONE_ATTRIBUTE));
+                       cnd = (String) attrs.get(DataModelNamespace.CND);
+                       List<DataModel> req = new ArrayList<>();
+                       for (BundleWire wire : requiredDataModels) {
+                               String requiredDataModelName = (String) wire.getCapability().getAttributes()
+                                               .get(DataModelNamespace.NAME);
+                               assert requiredDataModelName != null;
+                               DataModel requiredDataModel = dataModels.get(requiredDataModelName);
+                               if (requiredDataModel == null)
+                                       throw new CmsException("No required data model " + requiredDataModelName);
+                               req.add(requiredDataModel);
+                       }
+                       required = Collections.unmodifiableList(req);
+               }
+
+               public String getName() {
+                       return name;
+               }
+
+               public boolean isAbstract() {
+                       return abstrct;
+               }
+
+               // public boolean isStandalone() {
+               // return !isAbstract();
+               // }
+
+               public String getCnd() {
+                       return cnd;
+               }
+
+               public List<DataModel> getRequired() {
+                       return required;
+               }
+
+               // @Override
+               // public int compareTo(DataModel o) {
+               // if (equals(o))
+               // return 0;
+               // int res = required(this, o, 0);
+               // if (res != 0)
+               // return res;
+               // // the other way round
+               // res = required(o, this, 0);
+               // if (res != 0)
+               // return -res;
+               // return 0;
+               // }
+
+               @Override
+               public int hashCode() {
+                       return name.hashCode();
+               }
+
+               @Override
+               public boolean equals(Object obj) {
+                       if (obj instanceof DataModel)
+                               return ((DataModel) obj).name.equals(name);
+                       return false;
+               }
+
+               @Override
+               public String toString() {
+                       return "Data model " + name;
+               }
+
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/DeployConfig.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/DeployConfig.java
new file mode 100644 (file)
index 0000000..8ad51fc
--- /dev/null
@@ -0,0 +1,296 @@
+package org.argeo.cms.internal.kernel;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.naming.InvalidNameException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.naming.AttributesDictionary;
+import org.argeo.naming.LdifParser;
+import org.argeo.naming.LdifWriter;
+import org.argeo.node.NodeConstants;
+import org.argeo.osgi.useradmin.UserAdminConf;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.cm.ConfigurationEvent;
+import org.osgi.service.cm.ConfigurationListener;
+
+class DeployConfig implements ConfigurationListener {
+       private final Log log = LogFactory.getLog(getClass());
+       private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+       private static Path deployConfigPath = KernelUtils.getOsgiInstancePath(KernelConstants.DEPLOY_CONFIG_PATH);
+       private SortedMap<LdapName, Attributes> deployConfigs = new TreeMap<>();
+       private final DataModels dataModels;
+
+       public DeployConfig(ConfigurationAdmin configurationAdmin, DataModels dataModels, boolean isClean) {
+               this.dataModels = dataModels;
+               // ConfigurationAdmin configurationAdmin =
+               // bc.getService(bc.getServiceReference(ConfigurationAdmin.class));
+               try {
+                       boolean isFirstInit = false;
+                       if (!isInitialized()) { // first init
+                               isFirstInit = true;
+                               firstInit();
+                       }
+                       init(configurationAdmin, isClean, isFirstInit);
+               } catch (IOException e) {
+                       throw new CmsException("Could not init deploy configs", e);
+               }
+               // FIXME check race conditions during initialization
+               // bc.registerService(ConfigurationListener.class, this, null);
+       }
+
+       private void firstInit() throws IOException {
+               log.info("## FIRST INIT ##");
+               Files.createDirectories(deployConfigPath.getParent());
+
+               // FirstInit firstInit = new FirstInit();
+               InitUtils.prepareFirstInitInstanceArea();
+
+               if (!Files.exists(deployConfigPath))
+                       deployConfigs = new TreeMap<>();
+               else// config file could have juste been copied by preparation
+                       try (InputStream in = Files.newInputStream(deployConfigPath)) {
+                               deployConfigs = new LdifParser().read(in);
+                       }
+               save();
+       }
+
+       private void setFromFrameworkProperties(boolean isFirstInit) {
+               // node repository
+               Dictionary<String, Object> nodeConfig = InitUtils
+                               .getNodeRepositoryConfig(getProps(NodeConstants.NODE_REPOS_FACTORY_PID, NodeConstants.NODE));
+               // node repository is mandatory
+               putFactoryDeployConfig(NodeConstants.NODE_REPOS_FACTORY_PID, nodeConfig);
+
+               // additional repositories
+               dataModels: for (DataModels.DataModel dataModel : dataModels.getNonAbstractDataModels()) {
+                       if (NodeConstants.NODE.equals(dataModel.getName()))
+                               continue dataModels;
+                       Dictionary<String, Object> config = InitUtils.getRepositoryConfig(dataModel.getName(),
+                                       getProps(NodeConstants.NODE_REPOS_FACTORY_PID, dataModel.getName()));
+                       if (config.size() != 0)
+                               putFactoryDeployConfig(NodeConstants.NODE_REPOS_FACTORY_PID, config);
+               }
+
+               // user admin
+               List<Dictionary<String, Object>> userDirectoryConfigs = InitUtils.getUserDirectoryConfigs();
+               if (userDirectoryConfigs.size() != 0) {
+                       List<String> activeCns = new ArrayList<>();
+                       for (int i = 0; i < userDirectoryConfigs.size(); i++) {
+                               Dictionary<String, Object> userDirectoryConfig = userDirectoryConfigs.get(i);
+                               String cn = UserAdminConf.baseDnHash(userDirectoryConfig);
+                               activeCns.add(cn);
+                               userDirectoryConfig.put(NodeConstants.CN, cn);
+                               putFactoryDeployConfig(NodeConstants.NODE_USER_ADMIN_PID, userDirectoryConfig);
+                       }
+                       // disable others
+                       LdapName userAdminFactoryName = serviceFactoryDn(NodeConstants.NODE_USER_ADMIN_PID);
+                       for (LdapName name : deployConfigs.keySet()) {
+                               if (name.startsWith(userAdminFactoryName) && !name.equals(userAdminFactoryName)) {
+                                       try {
+                                               Attributes attrs = deployConfigs.get(name);
+                                               String cn = name.getRdn(name.size() - 1).getValue().toString();
+                                               if (!activeCns.contains(cn)) {
+                                                       attrs.put(UserAdminConf.disabled.name(), "true");
+                                               }
+                                       } catch (Exception e) {
+                                               throw new CmsException("Cannot disable user directory " + name, e);
+                                       }
+                               }
+                       }
+               }
+
+               // http server
+               Dictionary<String, Object> webServerConfig = InitUtils
+                               .getHttpServerConfig(getProps(KernelConstants.JETTY_FACTORY_PID, NodeConstants.DEFAULT));
+               if (!webServerConfig.isEmpty())
+                       putFactoryDeployConfig(KernelConstants.JETTY_FACTORY_PID, webServerConfig);
+
+               save();
+       }
+
+       private void init(ConfigurationAdmin configurationAdmin, boolean isClean, boolean isFirstInit) throws IOException {
+
+               try (InputStream in = Files.newInputStream(deployConfigPath)) {
+                       deployConfigs = new LdifParser().read(in);
+               }
+               if (isClean) {
+                       setFromFrameworkProperties(isFirstInit);
+                       for (LdapName dn : deployConfigs.keySet()) {
+                               Rdn lastRdn = dn.getRdn(dn.size() - 1);
+                               LdapName prefix = (LdapName) dn.getPrefix(dn.size() - 1);
+                               if (prefix.toString().equals(NodeConstants.DEPLOY_BASEDN)) {
+                                       if (lastRdn.getType().equals(NodeConstants.CN)) {
+                                               // service
+                                               String pid = lastRdn.getValue().toString();
+                                               Configuration conf = configurationAdmin.getConfiguration(pid);
+                                               AttributesDictionary dico = new AttributesDictionary(deployConfigs.get(dn));
+                                               conf.update(dico);
+                                       } else {
+                                               // service factory definition
+                                       }
+                               } else {
+                                       // service factory service
+                                       Rdn beforeLastRdn = dn.getRdn(dn.size() - 2);
+                                       assert beforeLastRdn.getType().equals(NodeConstants.OU);
+                                       String factoryPid = beforeLastRdn.getValue().toString();
+                                       Configuration conf = configurationAdmin.createFactoryConfiguration(factoryPid.toString(), null);
+                                       AttributesDictionary dico = new AttributesDictionary(deployConfigs.get(dn));
+                                       conf.update(dico);
+                               }
+                       }
+               }
+               // TODO check consistency if not clean
+       }
+
+       @Override
+       public void configurationEvent(ConfigurationEvent event) {
+               try {
+                       if (ConfigurationEvent.CM_UPDATED == event.getType()) {
+                               ConfigurationAdmin configurationAdmin = bc.getService(event.getReference());
+                               Configuration conf = configurationAdmin.getConfiguration(event.getPid(), null);
+                               LdapName serviceDn = null;
+                               String factoryPid = conf.getFactoryPid();
+                               if (factoryPid != null) {
+                                       LdapName serviceFactoryDn = serviceFactoryDn(factoryPid);
+                                       if (deployConfigs.containsKey(serviceFactoryDn)) {
+                                               for (LdapName dn : deployConfigs.keySet()) {
+                                                       if (dn.startsWith(serviceFactoryDn)) {
+                                                               Rdn lastRdn = dn.getRdn(dn.size() - 1);
+                                                               assert lastRdn.getType().equals(NodeConstants.CN);
+                                                               Object value = conf.getProperties().get(lastRdn.getType());
+                                                               assert value != null;
+                                                               if (value.equals(lastRdn.getValue())) {
+                                                                       serviceDn = dn;
+                                                                       break;
+                                                               }
+                                                       }
+                                               }
+
+                                               Object cn = conf.getProperties().get(NodeConstants.CN);
+                                               if (cn == null)
+                                                       throw new IllegalArgumentException("Properties must contain cn");
+                                               if (serviceDn == null) {
+                                                       putFactoryDeployConfig(factoryPid, conf.getProperties());
+                                               } else {
+                                                       Attributes attrs = deployConfigs.get(serviceDn);
+                                                       assert attrs != null;
+                                                       AttributesDictionary.copy(conf.getProperties(), attrs);
+                                               }
+                                               save();
+                                               if (log.isDebugEnabled())
+                                                       log.debug("Updated deploy config " + serviceDn(factoryPid, cn.toString()));
+                                       } else {
+                                               // ignore non config-registered service factories
+                                       }
+                               } else {
+                                       serviceDn = serviceDn(event.getPid());
+                                       if (deployConfigs.containsKey(serviceDn)) {
+                                               Attributes attrs = deployConfigs.get(serviceDn);
+                                               assert attrs != null;
+                                               AttributesDictionary.copy(conf.getProperties(), attrs);
+                                               save();
+                                               if (log.isDebugEnabled())
+                                                       log.debug("Updated deploy config " + serviceDn);
+                                       } else {
+                                               // ignore non config-registered services
+                                       }
+                               }
+                       }
+               } catch (Exception e) {
+                       log.error("Could not handle configuration event", e);
+               }
+       }
+
+       void putFactoryDeployConfig(String factoryPid, Dictionary<String, Object> props) {
+               Object cn = props.get(NodeConstants.CN);
+               if (cn == null)
+                       throw new IllegalArgumentException("cn must be set in properties");
+               LdapName serviceFactoryDn = serviceFactoryDn(factoryPid);
+               if (!deployConfigs.containsKey(serviceFactoryDn))
+                       deployConfigs.put(serviceFactoryDn, new BasicAttributes(NodeConstants.OU, factoryPid));
+               LdapName serviceDn = serviceDn(factoryPid, cn.toString());
+               Attributes attrs = new BasicAttributes();
+               AttributesDictionary.copy(props, attrs);
+               deployConfigs.put(serviceDn, attrs);
+       }
+
+       void putDeployConfig(String servicePid, Dictionary<String, Object> props) {
+               LdapName serviceDn = serviceDn(servicePid);
+               Attributes attrs = new BasicAttributes(NodeConstants.CN, servicePid);
+               AttributesDictionary.copy(props, attrs);
+               deployConfigs.put(serviceDn, attrs);
+       }
+
+       void save() {
+               try (Writer writer = Files.newBufferedWriter(deployConfigPath)) {
+                       new LdifWriter(writer).write(deployConfigs);
+               } catch (IOException e) {
+                       // throw new CmsException("Cannot save deploy configs", e);
+                       log.error("Cannot save deploy configs", e);
+               }
+       }
+
+       boolean isStandalone(String dataModelName) {
+               return getProps(NodeConstants.NODE_REPOS_FACTORY_PID, dataModelName) != null;
+       }
+
+       /*
+        * UTILITIES
+        */
+       private LdapName serviceFactoryDn(String factoryPid) {
+               try {
+                       return new LdapName(NodeConstants.OU + "=" + factoryPid + "," + NodeConstants.DEPLOY_BASEDN);
+               } catch (InvalidNameException e) {
+                       throw new IllegalArgumentException("Cannot generate DN from " + factoryPid, e);
+               }
+       }
+
+       private LdapName serviceDn(String servicePid) {
+               try {
+                       return new LdapName(NodeConstants.CN + "=" + servicePid + "," + NodeConstants.DEPLOY_BASEDN);
+               } catch (InvalidNameException e) {
+                       throw new IllegalArgumentException("Cannot generate DN from " + servicePid, e);
+               }
+       }
+
+       private LdapName serviceDn(String factoryPid, String cn) {
+               try {
+                       return (LdapName) serviceFactoryDn(factoryPid).add(new Rdn(NodeConstants.CN, cn));
+               } catch (InvalidNameException e) {
+                       throw new IllegalArgumentException("Cannot generate DN from " + factoryPid + " and " + cn, e);
+               }
+       }
+
+       Dictionary<String, Object> getProps(String factoryPid, String cn) {
+               Attributes attrs = deployConfigs.get(serviceDn(factoryPid, cn));
+               if (attrs != null)
+                       return new AttributesDictionary(attrs);
+               else
+                       return null;
+       }
+
+       static boolean isInitialized() {
+               return Files.exists(deployConfigPath);
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/GogoShellKiller.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/GogoShellKiller.java
new file mode 100644 (file)
index 0000000..39b11a5
--- /dev/null
@@ -0,0 +1,64 @@
+package org.argeo.cms.internal.kernel;
+
+/**
+ * Workaround for killing Gogo shell by system shutdown.
+ * 
+ * @see https://issues.apache.org/jira/browse/FELIX-4208
+ */
+class GogoShellKiller extends Thread {
+
+       public GogoShellKiller() {
+               super("Gogo Shell Killer");
+               setDaemon(true);
+       }
+
+       @Override
+       public void run() {
+               ThreadGroup rootTg = getRootThreadGroup(null);
+               Thread gogoShellThread = findGogoShellThread(rootTg);
+               if (gogoShellThread == null)
+                       return;
+               while (getNonDaemonCount(rootTg) > 2) {
+                       try {
+                               Thread.sleep(100);
+                       } catch (InterruptedException e) {
+                               // silent
+                       }
+               }
+               gogoShellThread = findGogoShellThread(rootTg);
+               if (gogoShellThread == null)
+                       return;
+               // No non-deamon threads left, forcibly halting the VM
+               Runtime.getRuntime().halt(0);
+       }
+
+       private ThreadGroup getRootThreadGroup(ThreadGroup tg) {
+               if (tg == null)
+                       tg = Thread.currentThread().getThreadGroup();
+               if (tg.getParent() == null)
+                       return tg;
+               else
+                       return getRootThreadGroup(tg.getParent());
+       }
+
+       private int getNonDaemonCount(ThreadGroup rootThreadGroup) {
+               Thread[] threads = new Thread[rootThreadGroup.activeCount()];
+               rootThreadGroup.enumerate(threads);
+               int nonDameonCount = 0;
+               for (Thread t : threads)
+                       if (t != null && !t.isDaemon())
+                               nonDameonCount++;
+               return nonDameonCount;
+       }
+
+       private Thread findGogoShellThread(ThreadGroup rootThreadGroup) {
+               Thread[] threads = new Thread[rootThreadGroup.activeCount()];
+               rootThreadGroup.enumerate(threads, true);
+               for (Thread thread : threads) {
+                       if (thread.getName().equals("pipe-gosh --login --noshutdown"))
+                               return thread;
+               }
+               return null;
+       }
+
+}
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/HomeRepository.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/HomeRepository.java
new file mode 100644 (file)
index 0000000..b4f65be
--- /dev/null
@@ -0,0 +1,189 @@
+package org.argeo.cms.internal.kernel;
+
+import java.security.PrivilegedAction;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.security.Privilege;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+
+import org.argeo.cms.CmsException;
+import org.argeo.jcr.JcrRepositoryWrapper;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.NodeNames;
+import org.argeo.node.NodeTypes;
+import org.argeo.node.NodeUtils;
+
+/**
+ * Make sure each user has a home directory available in the default workspace.
+ */
+class HomeRepository extends JcrRepositoryWrapper implements KernelConstants {
+
+       /** The home base path. */
+       private String homeBasePath = KernelConstants.DEFAULT_HOME_BASE_PATH;
+       private String usersBasePath = KernelConstants.DEFAULT_USERS_BASE_PATH;
+       private String groupsBasePath = KernelConstants.DEFAULT_GROUPS_BASE_PATH;
+
+       private Set<String> checkedUsers = new HashSet<String>();
+
+       private SimpleDateFormat usersDatePath = new SimpleDateFormat("YYYY/MM");
+
+       private final boolean remote;
+
+       public HomeRepository(Repository repository, boolean remote) {
+               super(repository);
+               this.remote = remote;
+               putDescriptor(NodeConstants.CN, NodeConstants.HOME);
+               if (!remote) {
+                       LoginContext lc;
+                       try {
+                               lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_DATA_ADMIN);
+                               lc.login();
+                       } catch (javax.security.auth.login.LoginException e1) {
+                               throw new CmsException("Cannot login as systrem", e1);
+                       }
+                       Subject.doAs(lc.getSubject(), new PrivilegedAction<Void>() {
+
+                               @Override
+                               public Void run() {
+                                       try {
+                                               Session adminSession = getRepository().login();
+                                               initJcr(adminSession);
+                                       } catch (RepositoryException e) {
+                                               throw new CmsException("Cannot init JCR home", e);
+                                       }
+                                       return null;
+                               }
+
+                       });
+               }
+       }
+
+       @Override
+       protected void processNewSession(Session session) {
+               String username = session.getUserID();
+               if (username == null || username.toString().equals(""))
+                       return;
+               if (session.getUserID().equals(NodeConstants.ROLE_ANONYMOUS))
+                       return;
+
+               if (checkedUsers.contains(username))
+                       return;
+               Session adminSession = KernelUtils.openAdminSession(getRepository(), session.getWorkspace().getName());
+               try {
+                       syncJcr(adminSession, username);
+                       checkedUsers.add(username);
+               } finally {
+                       JcrUtils.logoutQuietly(adminSession);
+               }
+       }
+
+       /*
+        * JCR
+        */
+       /** Session is logged out. */
+       private void initJcr(Session adminSession) {
+               try {
+                       JcrUtils.mkdirs(adminSession, homeBasePath);
+                       JcrUtils.mkdirs(adminSession, groupsBasePath);
+                       adminSession.save();
+
+                       JcrUtils.addPrivilege(adminSession, homeBasePath, NodeConstants.ROLE_USER_ADMIN, Privilege.JCR_READ);
+                       JcrUtils.addPrivilege(adminSession, groupsBasePath, NodeConstants.ROLE_USER_ADMIN, Privilege.JCR_READ);
+                       adminSession.save();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot initialize home repository", e);
+               } finally {
+                       JcrUtils.logoutQuietly(adminSession);
+               }
+       }
+
+       private void syncJcr(Session session, String username) {
+               try {
+                       Node userHome = NodeUtils.getUserHome(session, username);
+                       if (userHome == null) {
+                               String homePath = generateUserPath(username);
+                               if (session.itemExists(homePath))// duplicate user id
+                                       userHome = session.getNode(homePath).getParent().addNode(JcrUtils.lastPathElement(homePath));
+                               else
+                                       userHome = JcrUtils.mkdirs(session, homePath);
+                               // userHome = JcrUtils.mkfolders(session, homePath);
+                               userHome.addMixin(NodeTypes.NODE_USER_HOME);
+                               userHome.setProperty(NodeNames.LDAP_UID, username);
+                               session.save();
+
+                               JcrUtils.clearAccessControList(session, homePath, username);
+                               JcrUtils.addPrivilege(session, homePath, username, Privilege.JCR_ALL);
+                       }
+                       if (session.hasPendingChanges())
+                               session.save();
+               } catch (RepositoryException e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new CmsException("Cannot sync node security model for " + username, e);
+               }
+       }
+
+       /** Generate path for a new user home */
+       private String generateUserPath(String username) {
+               LdapName dn;
+               try {
+                       dn = new LdapName(username);
+               } catch (InvalidNameException e) {
+                       throw new CmsException("Invalid name " + username, e);
+               }
+               String userId = dn.getRdn(dn.size() - 1).getValue().toString();
+               int atIndex = userId.indexOf('@');
+               if (atIndex < 0) {
+                       return homeBasePath + '/' + userId;
+               } else {
+                       return usersBasePath + '/' + usersDatePath.format(new Date()) + '/' + userId;
+               }
+               // if (atIndex > 0) {
+               // String domain = userId.substring(0, atIndex);
+               // String name = userId.substring(atIndex + 1);
+               // return base + '/' + domain + '/' + name;
+               // } else if (atIndex == 0 || atIndex == (userId.length() - 1)) {
+               // throw new CmsException("Unsupported username " + userId);
+               // } else {
+               // return base + '/' + userId;
+               // }
+       }
+
+       public void createWorkgroup(LdapName dn) {
+               Session adminSession = KernelUtils.openAdminSession(this);
+               String cn = dn.getRdn(dn.size() - 1).getValue().toString();
+               Node newWorkgroup = NodeUtils.getGroupHome(adminSession, cn);
+               if (newWorkgroup != null) {
+                       JcrUtils.logoutQuietly(adminSession);
+                       throw new CmsException("Workgroup " + newWorkgroup + " already exists for " + dn);
+               }
+               try {
+                       // TODO enhance transformation of cn to a valid node name
+                       // String relPath = cn.replaceAll("[^a-zA-Z0-9]", "_");
+                       String relPath = JcrUtils.replaceInvalidChars(cn);
+                       newWorkgroup = JcrUtils.mkdirs(adminSession.getNode(groupsBasePath), relPath, NodeType.NT_UNSTRUCTURED);
+                       newWorkgroup.addMixin(NodeTypes.NODE_GROUP_HOME);
+                       newWorkgroup.setProperty(NodeNames.LDAP_CN, cn);
+                       adminSession.save();
+                       JcrUtils.addPrivilege(adminSession, newWorkgroup.getPath(), dn.toString(), Privilege.JCR_ALL);
+                       adminSession.save();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot create workgroup", e);
+               } finally {
+                       JcrUtils.logoutQuietly(adminSession);
+               }
+
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/InitUtils.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/InitUtils.java
new file mode 100644 (file)
index 0000000..a489250
--- /dev/null
@@ -0,0 +1,259 @@
+package org.argeo.cms.internal.kernel;
+
+import static org.argeo.cms.internal.kernel.KernelUtils.getFrameworkProp;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.internal.http.HttpConstants;
+import org.argeo.cms.internal.jcr.RepoConf;
+import org.argeo.node.NodeConstants;
+import org.argeo.osgi.useradmin.UserAdminConf;
+
+/**
+ * Interprets framework properties in order to generate the initial deploy
+ * configuration.
+ */
+class InitUtils {
+       private final static Log log = LogFactory.getLog(InitUtils.class);
+
+       /** Override the provided config with the framework properties */
+       static Dictionary<String, Object> getNodeRepositoryConfig(Dictionary<String, Object> provided) {
+               Dictionary<String, Object> props = provided != null ? provided : new Hashtable<String, Object>();
+               for (RepoConf repoConf : RepoConf.values()) {
+                       Object value = getFrameworkProp(NodeConstants.NODE_REPO_PROP_PREFIX + repoConf.name());
+                       if (value != null)
+                               props.put(repoConf.name(), value);
+               }
+               props.put(NodeConstants.CN, NodeConstants.NODE);
+               return props;
+       }
+
+       static Dictionary<String, Object> getRepositoryConfig(String dataModelName, Dictionary<String, Object> provided) {
+               if (dataModelName.equals(NodeConstants.NODE) || dataModelName.equals(NodeConstants.HOME))
+                       throw new IllegalArgumentException("Data model '" + dataModelName + "' is reserved.");
+               Dictionary<String, Object> props = provided != null ? provided : new Hashtable<String, Object>();
+               for (RepoConf repoConf : RepoConf.values()) {
+                       Object value = getFrameworkProp(
+                                       NodeConstants.NODE_REPOS_PROP_PREFIX + dataModelName + '.' + repoConf.name());
+                       if (value != null)
+                               props.put(repoConf.name(), value);
+               }
+               if (props.size() != 0)
+                       props.put(NodeConstants.CN, dataModelName);
+               return props;
+       }
+
+       /** Override the provided config with the framework properties */
+       static Dictionary<String, Object> getHttpServerConfig(Dictionary<String, Object> provided) {
+               String httpPort = getFrameworkProp("org.osgi.service.http.port");
+               String httpsPort = getFrameworkProp("org.osgi.service.http.port.secure");
+               /// TODO make it more generic
+               String httpHost = getFrameworkProp(HttpConstants.JETTY_PROPERTY_PREFIX + HttpConstants.HTTP_HOST);
+               String httpsHost = getFrameworkProp(HttpConstants.JETTY_PROPERTY_PREFIX + HttpConstants.HTTPS_HOST);
+
+               final Hashtable<String, Object> props = new Hashtable<String, Object>();
+               // try {
+               if (httpPort != null || httpsPort != null) {
+                       if (httpPort != null) {
+                               props.put(HttpConstants.HTTP_PORT, httpPort);
+                               props.put(HttpConstants.HTTP_ENABLED, true);
+                       }
+                       if (httpsPort != null) {
+                               props.put(HttpConstants.HTTPS_PORT, httpsPort);
+                               props.put(HttpConstants.HTTPS_ENABLED, true);
+                               Path keyStorePath = KernelUtils.getOsgiInstancePath(KernelConstants.DEFAULT_KEYSTORE_PATH);
+                               String keyStorePassword = getFrameworkProp(
+                                               HttpConstants.JETTY_PROPERTY_PREFIX + HttpConstants.SSL_PASSWORD);
+                               if (keyStorePassword == null)
+                                       keyStorePassword = "changeit";
+                               if (!Files.exists(keyStorePath))
+                                       createSelfSignedKeyStore(keyStorePath, keyStorePassword);
+                               props.put(HttpConstants.SSL_KEYSTORETYPE, "PKCS12");
+                               props.put(HttpConstants.SSL_KEYSTORE, keyStorePath.toString());
+                               props.put(HttpConstants.SSL_PASSWORD, keyStorePassword);
+                               props.put(HttpConstants.SSL_WANTCLIENTAUTH, true);
+                               String needClientAuth = getFrameworkProp(
+                                               HttpConstants.JETTY_PROPERTY_PREFIX + HttpConstants.SSL_NEEDCLIENTAUTH);
+                               if (needClientAuth != null) {
+                                       props.put(HttpConstants.SSL_NEEDCLIENTAUTH, Boolean.parseBoolean(needClientAuth));
+                               }
+                       }
+                       if (httpHost != null)
+                               props.put(HttpConstants.HTTP_HOST, httpHost);
+                       if (httpsHost != null)
+                               props.put(HttpConstants.HTTPS_HOST, httpHost);
+
+                       props.put(NodeConstants.CN, NodeConstants.DEFAULT);
+               }
+               return props;
+       }
+
+       static List<Dictionary<String, Object>> getUserDirectoryConfigs() {
+               List<Dictionary<String, Object>> res = new ArrayList<>();
+               File nodeBaseDir = KernelUtils.getOsgiInstancePath(KernelConstants.DIR_NODE).toFile();
+               List<String> uris = new ArrayList<>();
+
+               // node roles
+               String nodeRolesUri = getFrameworkProp(NodeConstants.ROLES_URI);
+               String baseNodeRoleDn = NodeConstants.ROLES_BASEDN;
+               if (nodeRolesUri == null) {
+                       nodeRolesUri = baseNodeRoleDn + ".ldif";
+                       File nodeRolesFile = new File(nodeBaseDir, nodeRolesUri);
+                       if (!nodeRolesFile.exists())
+                               try {
+                                       FileUtils.copyInputStreamToFile(InitUtils.class.getResourceAsStream(baseNodeRoleDn + ".ldif"),
+                                                       nodeRolesFile);
+                               } catch (IOException e) {
+                                       throw new CmsException("Cannot copy demo resource", e);
+                               }
+                       // nodeRolesUri = nodeRolesFile.toURI().toString();
+               }
+               uris.add(nodeRolesUri);
+
+               // node tokens
+               String nodeTokensUri = getFrameworkProp(NodeConstants.TOKENS_URI);
+               String baseNodeTokensDn = NodeConstants.TOKENS_BASEDN;
+               if (nodeTokensUri == null) {
+                       nodeTokensUri = baseNodeTokensDn + ".ldif";
+                       File nodeRolesFile = new File(nodeBaseDir, nodeRolesUri);
+                       if (!nodeRolesFile.exists())
+                               try {
+                                       FileUtils.copyInputStreamToFile(InitUtils.class.getResourceAsStream(baseNodeTokensDn + ".ldif"),
+                                                       nodeRolesFile);
+                               } catch (IOException e) {
+                                       throw new CmsException("Cannot copy demo resource", e);
+                               }
+                       // nodeRolesUri = nodeRolesFile.toURI().toString();
+               }
+               uris.add(nodeTokensUri);
+
+               // Business roles
+               String userAdminUris = getFrameworkProp(NodeConstants.USERADMIN_URIS);
+               if (userAdminUris == null) {
+                       String demoBaseDn = "dc=example,dc=com";
+                       userAdminUris = demoBaseDn + ".ldif";
+                       File businessRolesFile = new File(nodeBaseDir, userAdminUris);
+                       if (!businessRolesFile.exists())
+                               try {
+                                       FileUtils.copyInputStreamToFile(InitUtils.class.getResourceAsStream(demoBaseDn + ".ldif"),
+                                                       businessRolesFile);
+                               } catch (IOException e) {
+                                       throw new CmsException("Cannot copy demo resource", e);
+                               }
+                       // userAdminUris = businessRolesFile.toURI().toString();
+                       log.warn("## DEV Using dummy base DN " + demoBaseDn);
+                       // TODO downgrade security level
+               }
+               for (String userAdminUri : userAdminUris.split(" "))
+                       uris.add(userAdminUri);
+
+               // Interprets URIs
+               for (String uri : uris) {
+                       URI u;
+                       try {
+                               u = new URI(uri);
+                               if (u.getPath() == null)
+                                       throw new CmsException("URI " + uri + " must have a path in order to determine base DN");
+                               if (u.getScheme() == null) {
+                                       if (uri.startsWith("/") || uri.startsWith("./") || uri.startsWith("../"))
+                                               u = new File(uri).getCanonicalFile().toURI();
+                                       else if (!uri.contains("/")) {
+                                               // u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + uri);
+                                               u = new URI(uri);
+                                       } else
+                                               throw new CmsException("Cannot interpret " + uri + " as an uri");
+                               } else if (u.getScheme().equals(UserAdminConf.SCHEME_FILE)) {
+                                       u = new File(u).getCanonicalFile().toURI();
+                               }
+                       } catch (Exception e) {
+                               throw new CmsException("Cannot interpret " + uri + " as an uri", e);
+                       }
+                       Dictionary<String, Object> properties = UserAdminConf.uriAsProperties(u.toString());
+                       res.add(properties);
+               }
+
+               return res;
+       }
+
+       /**
+        * Called before node initialisation, in order populate OSGi instance are with
+        * some files (typically LDIF, etc).
+        */
+       static void prepareFirstInitInstanceArea() {
+               String nodeInit = getFrameworkProp(NodeConstants.NODE_INIT);
+               if (nodeInit == null)
+                       nodeInit = "../../init";
+               if (nodeInit.startsWith("http")) {
+                       // remoteFirstInit(nodeInit);
+                       return;
+               }
+
+               // TODO use java.nio.file
+               File initDir;
+               if (nodeInit.startsWith("."))
+                       initDir = KernelUtils.getExecutionDir(nodeInit);
+               else
+                       initDir = new File(nodeInit);
+               // TODO also uncompress archives
+               if (initDir.exists())
+                       try {
+                               FileUtils.copyDirectory(initDir, KernelUtils.getOsgiInstanceDir(), new FileFilter() {
+
+                                       @Override
+                                       public boolean accept(File pathname) {
+                                               if (pathname.getName().equals(".svn") || pathname.getName().equals(".git"))
+                                                       return false;
+                                               return true;
+                                       }
+                               });
+                               log.info("CMS initialized from " + initDir.getCanonicalPath());
+                       } catch (IOException e) {
+                               throw new CmsException("Cannot initialize from " + initDir, e);
+                       }
+       }
+
+       private static void createSelfSignedKeyStore(Path keyStorePath, String keyStorePassword) {
+               // for (Provider provider : Security.getProviders())
+               // System.out.println(provider.getName());
+               File keyStoreFile = keyStorePath.toFile();
+               char[] ksPwd = keyStorePassword.toCharArray();
+               char[] keyPwd = Arrays.copyOf(ksPwd, ksPwd.length);
+               if (!keyStoreFile.exists()) {
+                       try {
+                               keyStoreFile.getParentFile().mkdirs();
+                               KeyStore keyStore = PkiUtils.getKeyStore(keyStoreFile, ksPwd);
+                               PkiUtils.generateSelfSignedCertificate(keyStore,
+                                               new X500Principal("CN=" + InetAddress.getLocalHost().getHostName() + ",OU=UNSECURE,O=UNSECURE"),
+                                               1024, keyPwd);
+                               PkiUtils.saveKeyStore(keyStoreFile, ksPwd, keyStore);
+                               if (log.isDebugEnabled())
+                                       log.debug("Created self-signed unsecure keystore " + keyStoreFile);
+                       } catch (Exception e) {
+                               if (keyStoreFile.length() == 0)
+                                       keyStoreFile.delete();
+                               log.error("Cannot create keystore " + keyStoreFile, e);
+                       }
+               } else {
+                       throw new CmsException("Keystore " + keyStorePath + " already exists");
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelConstants.java
new file mode 100644 (file)
index 0000000..f221d0c
--- /dev/null
@@ -0,0 +1,45 @@
+package org.argeo.cms.internal.kernel;
+
+import org.argeo.node.NodeConstants;
+
+public interface KernelConstants {
+       String[] DEFAULT_CNDS = { "/org/argeo/jcr/argeo.cnd", "/org/argeo/cms/cms.cnd" };
+
+       // Directories
+       String DIR_NODE = "node";
+       String DIR_REPOS = "repos";
+       String DIR_INDEXES = "indexes";
+       String DIR_TRANSACTIONS = "transactions";
+
+       // Files
+       String DEPLOY_CONFIG_PATH = DIR_NODE + '/' + NodeConstants.DEPLOY_BASEDN + ".ldif";
+       String DEFAULT_KEYSTORE_PATH = DIR_NODE + '/' + NodeConstants.NODE + ".p12";
+       String NODE_KEY_TAB_PATH = DIR_NODE + "/krb5.keytab";
+
+       // Security
+       String JAAS_CONFIG = "/org/argeo/cms/internal/kernel/jaas.cfg";
+       String JAAS_CONFIG_IPA = "/org/argeo/cms/internal/kernel/jaas-ipa.cfg";
+
+       // Java
+       String JAAS_CONFIG_PROP = "java.security.auth.login.config";
+
+       // DEFAULTS JCR PATH
+       String DEFAULT_HOME_BASE_PATH = "/home";
+       String DEFAULT_USERS_BASE_PATH = "/users";
+       String DEFAULT_GROUPS_BASE_PATH = "/groups";
+
+       // HTTP client
+       String COOKIE_POLICY_BROWSER_COMPATIBILITY = "compatibility";
+
+       // RWT / RAP
+       // String PATH_WORKBENCH = "/ui";
+       // String PATH_WORKBENCH_PUBLIC = PATH_WORKBENCH + "/public";
+
+       String JETTY_FACTORY_PID = "org.eclipse.equinox.http.jetty.config";
+       String WHITEBOARD_PATTERN_PROP = "osgi.http.whiteboard.servlet.pattern";
+
+       // avoid dependencies
+       String CONTEXT_NAME_PROP = "contextName";
+       String JACKRABBIT_REPOSITORY_URI = "org.apache.jackrabbit.repository.uri";
+       String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "org.apache.jackrabbit.spi2davex.WorkspaceNameDefault";
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelThread.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelThread.java
new file mode 100644 (file)
index 0000000..b2b51eb
--- /dev/null
@@ -0,0 +1,123 @@
+package org.argeo.cms.internal.kernel;
+
+import java.awt.image.Kernel;
+import java.io.File;
+import java.lang.management.ManagementFactory;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.api.stats.RepositoryStatistics;
+import org.apache.jackrabbit.stats.RepositoryStatisticsImpl;
+import org.argeo.cms.internal.auth.CmsSessionImpl;
+
+/**
+ * Background thread started by the {@link Kernel}, which gather statistics and
+ * monitor/control other processes.
+ */
+class KernelThread extends Thread {
+       private final static Log log = LogFactory.getLog(KernelThread.class);
+
+       private RepositoryStatisticsImpl repoStats;
+
+       /** The smallest period of operation, in ms */
+       private final long PERIOD = 60 * 1000l;
+       /** One ms in ns */
+       private final static long m = 1000l * 1000l;
+       private final static long M = 1024l * 1024l;
+
+       private boolean running = true;
+
+       private Log kernelStatsLog = LogFactory.getLog("argeo.stats.kernel");
+       private Log nodeStatsLog = LogFactory.getLog("argeo.stats.node");
+
+       @SuppressWarnings("unused")
+       private long cycle = 0l;
+
+       public KernelThread(ThreadGroup threadGroup, String name) {
+               super(threadGroup, name);
+       }
+
+       private void doSmallestPeriod() {
+               // Clean expired sessions
+               CmsSessionImpl.closeInvalidSessions();
+
+               if (kernelStatsLog.isDebugEnabled()) {
+                       StringBuilder line = new StringBuilder(64);
+                       line.append("§\t");
+                       long freeMem = Runtime.getRuntime().freeMemory() / M;
+                       long totalMem = Runtime.getRuntime().totalMemory() / M;
+                       long maxMem = Runtime.getRuntime().maxMemory() / M;
+                       double loadAvg = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
+                       // in min
+                       boolean min = true;
+                       long uptime = ManagementFactory.getRuntimeMXBean().getUptime() / (1000 * 60);
+                       if (uptime > 24 * 60) {
+                               min = false;
+                               uptime = uptime / 60;
+                       }
+                       line.append(uptime).append(min ? " min" : " h").append('\t');
+                       line.append(loadAvg).append('\t').append(maxMem).append('\t').append(totalMem).append('\t').append(freeMem)
+                                       .append('\t');
+                       kernelStatsLog.debug(line);
+               }
+
+               if (nodeStatsLog.isDebugEnabled()) {
+                       File dataDir = KernelUtils.getOsgiInstanceDir();
+                       long freeSpace = dataDir.getUsableSpace() / M;
+                       // File currentRoot = null;
+                       // for (File root : File.listRoots()) {
+                       // String rootPath = root.getAbsolutePath();
+                       // if (dataDir.getAbsolutePath().startsWith(rootPath)) {
+                       // if (currentRoot == null
+                       // || (rootPath.length() > currentRoot.getPath()
+                       // .length())) {
+                       // currentRoot = root;
+                       // }
+                       // }
+                       // }
+                       // long totalSpace = currentRoot.getTotalSpace();
+                       StringBuilder line = new StringBuilder(128);
+                       line.append("§\t").append(freeSpace).append(" MB left in " + dataDir);
+                       line.append('\n');
+                       if (repoStats != null)
+                               for (RepositoryStatistics.Type type : RepositoryStatistics.Type.values()) {
+                                       long[] vals = repoStats.getTimeSeries(type).getValuePerMinute();
+                                       long val = vals[vals.length - 1];
+                                       line.append(type.name()).append('\t').append(val).append('\n');
+                               }
+                       nodeStatsLog.debug(line);
+               }
+       }
+
+       @Override
+       public void run() {
+               final long periodNs = PERIOD * m;
+               while (running) {
+                       long beginNs = System.nanoTime();
+                       doSmallestPeriod();
+
+                       long waitNs = periodNs - (System.nanoTime() - beginNs);
+                       if (waitNs < 0)
+                               continue;
+                       // wait
+                       try {
+                               sleep(waitNs / m, (int) (waitNs % m));
+                       } catch (InterruptedException e) {
+                               // silent
+                       }
+                       cycle++;
+               }
+       }
+
+       synchronized void destroyAndJoin() {
+               running = false;
+               notifyAll();
+//             interrupt();
+//             try {
+//                     join(PERIOD * 2);
+//             } catch (InterruptedException e) {
+//                     // throw new CmsException("Kernel thread destruction was interrupted");
+//                     log.error("Kernel thread destruction was interrupted", e);
+//             }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelUtils.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/KernelUtils.java
new file mode 100644 (file)
index 0000000..1d81409
--- /dev/null
@@ -0,0 +1,249 @@
+package org.argeo.cms.internal.kernel;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.PrivilegedAction;
+import java.security.URIParameter;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.commons.logging.Log;
+import org.argeo.cms.CmsException;
+import org.argeo.node.DataModelNamespace;
+import org.argeo.node.NodeConstants;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.util.tracker.ServiceTracker;
+
+/** Package utilities */
+class KernelUtils implements KernelConstants {
+       final static String OSGI_INSTANCE_AREA = "osgi.instance.area";
+       final static String OSGI_CONFIGURATION_AREA = "osgi.configuration.area";
+
+       static void setJaasConfiguration(URL jaasConfigurationUrl) {
+               try {
+                       URIParameter uriParameter = new URIParameter(jaasConfigurationUrl.toURI());
+                       javax.security.auth.login.Configuration jaasConfiguration = javax.security.auth.login.Configuration
+                                       .getInstance("JavaLoginConfig", uriParameter);
+                       javax.security.auth.login.Configuration.setConfiguration(jaasConfiguration);
+               } catch (Exception e) {
+                       throw new CmsException("Cannot set configuration " + jaasConfigurationUrl, e);
+               }
+       }
+
+       static Dictionary<String, ?> asDictionary(Properties props) {
+               Hashtable<String, Object> hashtable = new Hashtable<String, Object>();
+               for (Object key : props.keySet()) {
+                       hashtable.put(key.toString(), props.get(key));
+               }
+               return hashtable;
+       }
+
+       static Dictionary<String, ?> asDictionary(ClassLoader cl, String resource) {
+               Properties props = new Properties();
+               try {
+                       props.load(cl.getResourceAsStream(resource));
+               } catch (IOException e) {
+                       throw new CmsException("Cannot load " + resource + " from classpath", e);
+               }
+               return asDictionary(props);
+       }
+
+       static File getExecutionDir(String relativePath) {
+               File executionDir = new File(getFrameworkProp("user.dir"));
+               if (relativePath == null)
+                       return executionDir;
+               try {
+                       return new File(executionDir, relativePath).getCanonicalFile();
+               } catch (IOException e) {
+                       throw new CmsException("Cannot get canonical file", e);
+               }
+       }
+
+       static File getOsgiInstanceDir() {
+               return new File(getBundleContext().getProperty(OSGI_INSTANCE_AREA).substring("file:".length()))
+                               .getAbsoluteFile();
+       }
+
+       static Path getOsgiInstancePath(String relativePath) {
+               return Paths.get(getOsgiInstanceUri(relativePath));
+       }
+
+       static URI getOsgiInstanceUri(String relativePath) {
+               String osgiInstanceBaseUri = getFrameworkProp(OSGI_INSTANCE_AREA);
+               return safeUri(osgiInstanceBaseUri + (relativePath != null ? relativePath : ""));
+       }
+
+       // static String getOsgiInstancePath(String relativePath) {
+       // try {
+       // if (relativePath == null)
+       // return getOsgiInstanceDir().getCanonicalPath();
+       // else
+       // return new File(getOsgiInstanceDir(), relativePath).getCanonicalPath();
+       // } catch (IOException e) {
+       // throw new CmsException("Cannot get instance path for " + relativePath,
+       // e);
+       // }
+       // }
+
+       static File getOsgiConfigurationFile(String relativePath) {
+               try {
+                       return new File(new URI(getBundleContext().getProperty(OSGI_CONFIGURATION_AREA) + relativePath))
+                                       .getCanonicalFile();
+               } catch (Exception e) {
+                       throw new CmsException("Cannot get configuration file for " + relativePath, e);
+               }
+       }
+
+       static String getFrameworkProp(String key, String def) {
+               String value = getBundleContext().getProperty(key);
+               if (value == null)
+                       return def;
+               return value;
+       }
+
+       static String getFrameworkProp(String key) {
+               return getFrameworkProp(key, null);
+       }
+
+       // Security
+       // static Subject anonymousLogin() {
+       // Subject subject = new Subject();
+       // LoginContext lc;
+       // try {
+       // lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, subject);
+       // lc.login();
+       // return subject;
+       // } catch (LoginException e) {
+       // throw new CmsException("Cannot login as anonymous", e);
+       // }
+       // }
+
+       static void logFrameworkProperties(Log log) {
+               BundleContext bc = getBundleContext();
+               for (Object sysProp : new TreeSet<Object>(System.getProperties().keySet())) {
+                       log.debug(sysProp + "=" + bc.getProperty(sysProp.toString()));
+               }
+               // String[] keys = { Constants.FRAMEWORK_STORAGE,
+               // Constants.FRAMEWORK_OS_NAME, Constants.FRAMEWORK_OS_VERSION,
+               // Constants.FRAMEWORK_PROCESSOR, Constants.FRAMEWORK_SECURITY,
+               // Constants.FRAMEWORK_TRUST_REPOSITORIES,
+               // Constants.FRAMEWORK_WINDOWSYSTEM, Constants.FRAMEWORK_VENDOR,
+               // Constants.FRAMEWORK_VERSION, Constants.FRAMEWORK_STORAGE_CLEAN,
+               // Constants.FRAMEWORK_LANGUAGE, Constants.FRAMEWORK_UUID };
+               // for (String key : keys)
+               // log.debug(key + "=" + bc.getProperty(key));
+       }
+
+       static void printSystemProperties(PrintStream out) {
+               TreeMap<String, String> display = new TreeMap<>();
+               for (Object key : System.getProperties().keySet())
+                       display.put(key.toString(), System.getProperty(key.toString()));
+               for (String key : display.keySet())
+                       out.println(key + "=" + display.get(key));
+       }
+
+       static Session openAdminSession(Repository repository) {
+               return openAdminSession(repository, null);
+       }
+
+       static Session openAdminSession(final Repository repository, final String workspaceName) {
+               ClassLoader currentCl = Thread.currentThread().getContextClassLoader();
+               Thread.currentThread().setContextClassLoader(KernelUtils.class.getClassLoader());
+               LoginContext loginContext;
+               try {
+                       loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_DATA_ADMIN);
+                       loginContext.login();
+               } catch (LoginException e1) {
+                       throw new CmsException("Could not login as data admin", e1);
+               } finally {
+                       Thread.currentThread().setContextClassLoader(currentCl);
+               }
+               return Subject.doAs(loginContext.getSubject(), new PrivilegedAction<Session>() {
+
+                       @Override
+                       public Session run() {
+                               try {
+                                       return repository.login(workspaceName);
+                               } catch (RepositoryException e) {
+                                       throw new CmsException("Cannot open admin session", e);
+                               }
+                       }
+
+               });
+       }
+
+       static void asyncOpen(ServiceTracker<?, ?> st) {
+               Runnable run = new Runnable() {
+
+                       @Override
+                       public void run() {
+                               st.open();
+                       }
+               };
+               new Thread(run, "Open service tracker " + st).start();
+       }
+
+       /**
+        * @return the {@link BundleContext} of the {@link Bundle} which provided this
+        *         class, never null.
+        * @throws CmsException
+        *             if the related bundle is not active
+        */
+       static BundleContext getBundleContext(Class<?> clzz) {
+               Bundle bundle = FrameworkUtil.getBundle(clzz);
+               BundleContext bc = bundle.getBundleContext();
+               if (bc == null)
+                       throw new CmsException("Bundle " + bundle.getSymbolicName() + " is not active");
+               return bc;
+       }
+
+       static BundleContext getBundleContext() {
+               return getBundleContext(KernelUtils.class);
+       }
+
+       static boolean asBoolean(String value) {
+               if (value == null)
+                       return false;
+               switch (value) {
+               case "true":
+                       return true;
+               case "false":
+                       return false;
+               default:
+                       throw new CmsException("Unsupported value for attribute " + DataModelNamespace.ABSTRACT
+                                       + ": " + value);
+               }
+       }
+
+       private static URI safeUri(String uri) {
+               if (uri == null)
+                       throw new CmsException("URI cannot be null");
+               try {
+                       return new URI(uri);
+               } catch (URISyntaxException e) {
+                       throw new CmsException("Dadly formatted URI " + uri, e);
+               }
+       }
+
+       private KernelUtils() {
+
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/LocalRepository.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/LocalRepository.java
new file mode 100644 (file)
index 0000000..4356d18
--- /dev/null
@@ -0,0 +1,23 @@
+package org.argeo.cms.internal.kernel;
+
+import javax.jcr.Repository;
+
+import org.argeo.jcr.JcrRepositoryWrapper;
+import org.argeo.node.NodeConstants;
+
+class LocalRepository extends JcrRepositoryWrapper {
+       private final String cn;
+
+       public LocalRepository(Repository repository, String cn) {
+               super(repository);
+               this.cn = cn;
+               // Map<String, Object> attrs = dataModelCapability.getAttributes();
+               // cn = (String) attrs.get(DataModelNamespace.NAME);
+               putDescriptor(NodeConstants.CN, cn);
+       }
+
+       String getCn() {
+               return cn;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeAuthorization.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeAuthorization.java
new file mode 100644 (file)
index 0000000..416f3bf
--- /dev/null
@@ -0,0 +1,73 @@
+package org.argeo.cms.internal.kernel;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.osgi.service.useradmin.Authorization;
+
+class NodeAuthorization implements Authorization {
+       private final String name;
+       private final String displayName;
+       private final List<String> systemRoles;
+       private final List<String> roles;
+
+       public NodeAuthorization(String name, String displayName,
+                       Collection<String> systemRoles, String[] roles) {
+               this.name = new X500Principal(name).getName();
+               this.displayName = displayName;
+               this.systemRoles = Collections.unmodifiableList(new ArrayList<String>(
+                               systemRoles));
+               this.roles = Collections.unmodifiableList(Arrays.asList(roles));
+       }
+
+       @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/internal/kernel/NodeHttp.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeHttp.java
new file mode 100644 (file)
index 0000000..92f804d
--- /dev/null
@@ -0,0 +1,335 @@
+package org.argeo.cms.internal.kernel;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+
+import javax.jcr.Repository;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.server.SessionProvider;
+import org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet;
+import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.internal.http.CmsSessionProvider;
+import org.argeo.cms.internal.http.DataHttpContext;
+import org.argeo.cms.internal.http.HttpUtils;
+import org.argeo.cms.internal.http.LinkServlet;
+import org.argeo.cms.internal.http.PrivateHttpContext;
+import org.argeo.cms.internal.http.RobotServlet;
+import org.argeo.node.NodeConstants;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Intercepts and enriches http access, mainly focusing on security and
+ * transactionality.
+ */
+public class NodeHttp implements KernelConstants {
+       private final static Log log = LogFactory.getLog(NodeHttp.class);
+
+       public final static String DEFAULT_SERVICE = "HTTP";
+
+       private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+       private ServiceTracker<Repository, Repository> repositories;
+       private final ServiceTracker<HttpService, HttpService> httpServiceTracker;
+
+       private static String httpRealm = "Argeo";
+       private final boolean cleanState;
+
+       public NodeHttp(boolean cleanState) {
+               this.cleanState = cleanState;
+               httpServiceTracker = new PrepareHttpStc();
+               // httpServiceTracker.open();
+               KernelUtils.asyncOpen(httpServiceTracker);
+       }
+
+       public void destroy() {
+               if (repositories != null)
+                       repositories.close();
+       }
+
+       public static void registerRepositoryServlets(HttpService httpService, String alias, Repository repository) {
+               if (httpService == null)
+                       throw new CmsException("No HTTP service available");
+               try {
+                       registerWebdavServlet(httpService, alias, repository);
+                       registerRemotingServlet(httpService, alias, repository);
+                       if (NodeConstants.HOME.equals(alias))
+                               registerFilesServlet(httpService, alias, repository);
+                       if (log.isDebugEnabled())
+                               log.debug("Registered servlets for repository '" + alias + "'");
+               } catch (Exception e) {
+                       throw new CmsException("Could not register servlets for repository '" + alias + "'", e);
+               }
+       }
+
+       public static void unregisterRepositoryServlets(HttpService httpService, String alias) {
+               if (httpService == null)
+                       return;
+               try {
+                       httpService.unregister(webdavPath(alias));
+                       httpService.unregister(remotingPath(alias));
+                       if (NodeConstants.HOME.equals(alias))
+                               httpService.unregister(filesPath(alias));
+                       if (log.isDebugEnabled())
+                               log.debug("Unregistered servlets for repository '" + alias + "'");
+               } catch (Exception e) {
+                       log.error("Could not unregister servlets for repository '" + alias + "'", e);
+               }
+       }
+
+       static void registerWebdavServlet(HttpService httpService, String alias, Repository repository)
+                       throws NamespaceException, ServletException {
+               // WebdavServlet webdavServlet = new WebdavServlet(repository, new
+               // OpenInViewSessionProvider(alias));
+               WebdavServlet webdavServlet = new WebdavServlet(repository, new CmsSessionProvider(alias));
+               String path = webdavPath(alias);
+               Properties ip = new Properties();
+               ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_CONFIG, HttpUtils.WEBDAV_CONFIG);
+               ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
+               httpService.registerServlet(path, webdavServlet, ip, new DataHttpContext(httpRealm));
+       }
+
+       static void registerFilesServlet(HttpService httpService, String alias, Repository repository)
+                       throws NamespaceException, ServletException {
+               WebdavServlet filesServlet = new WebdavServlet(repository, new CmsSessionProvider(alias));
+               String path = filesPath(alias);
+               Properties ip = new Properties();
+               ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_CONFIG, HttpUtils.WEBDAV_CONFIG);
+               ip.setProperty(WebdavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
+               httpService.registerServlet(path, filesServlet, ip, new PrivateHttpContext(httpRealm, true));
+       }
+
+       static void registerRemotingServlet(HttpService httpService, String alias, Repository repository)
+                       throws NamespaceException, ServletException {
+               RemotingServlet remotingServlet = new RemotingServlet(repository, new CmsSessionProvider(alias));
+               String path = remotingPath(alias);
+               Properties ip = new Properties();
+               ip.setProperty(JcrRemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, path);
+               ip.setProperty(JcrRemotingServlet.INIT_PARAM_AUTHENTICATE_HEADER, "Negotiate");
+
+               // Looks like a bug in Jackrabbit remoting init
+               Path tmpDir;
+               try {
+                       tmpDir = Files.createTempDirectory("remoting_" + alias);
+               } catch (IOException e) {
+                       throw new CmsException("Cannot create temp directory for remoting servlet", e);
+               }
+               ip.setProperty(RemotingServlet.INIT_PARAM_HOME, tmpDir.toString());
+               ip.setProperty(RemotingServlet.INIT_PARAM_TMP_DIRECTORY, "remoting_" + alias);
+               ip.setProperty(RemotingServlet.INIT_PARAM_PROTECTED_HANDLERS_CONFIG, HttpUtils.DEFAULT_PROTECTED_HANDLERS);
+               ip.setProperty(RemotingServlet.INIT_PARAM_CREATE_ABSOLUTE_URI, "false");
+               httpService.registerServlet(path, remotingServlet, ip, new PrivateHttpContext(httpRealm));
+       }
+
+       static String webdavPath(String alias) {
+               return NodeConstants.PATH_DATA + "/" + alias;
+       }
+
+       static String remotingPath(String alias) {
+               return NodeConstants.PATH_JCR + "/" + alias;
+       }
+
+       static String filesPath(String alias) {
+               return NodeConstants.PATH_FILES;
+       }
+
+       // private Subject subjectFromRequest(HttpServletRequest request,
+       // HttpServletResponse response) {
+       // Authorization authorization = (Authorization)
+       // request.getAttribute(HttpContext.AUTHORIZATION);
+       // if (authorization == null)
+       // throw new CmsException("Not authenticated");
+       // try {
+       // LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER,
+       // new HttpRequestCallbackHandler(request, response));
+       // lc.login();
+       // return lc.getSubject();
+       // } catch (LoginException e) {
+       // throw new CmsException("Cannot login", e);
+       // }
+       // }
+
+       static class RepositoriesStc extends ServiceTracker<Repository, Repository> {
+               private final HttpService httpService;
+
+               private final BundleContext bc;
+
+               public RepositoriesStc(BundleContext bc, HttpService httpService) {
+                       super(bc, Repository.class, null);
+                       this.httpService = httpService;
+                       this.bc = bc;
+               }
+
+               @Override
+               public Repository addingService(ServiceReference<Repository> reference) {
+                       Repository repository = bc.getService(reference);
+                       Object jcrRepoAlias = reference.getProperty(NodeConstants.CN);
+                       if (jcrRepoAlias != null) {
+                               String alias = jcrRepoAlias.toString();
+                               registerRepositoryServlets(httpService, alias, repository);
+                       }
+                       return repository;
+               }
+
+               @Override
+               public void modifiedService(ServiceReference<Repository> reference, Repository service) {
+               }
+
+               @Override
+               public void removedService(ServiceReference<Repository> reference, Repository service) {
+                       Object jcrRepoAlias = reference.getProperty(NodeConstants.CN);
+                       if (jcrRepoAlias != null) {
+                               String alias = jcrRepoAlias.toString();
+                               unregisterRepositoryServlets(httpService, alias);
+                       }
+               }
+       }
+
+       private class PrepareHttpStc extends ServiceTracker<HttpService, HttpService> {
+               // private DataHttp dataHttp;
+               // private NodeHttp nodeHttp;
+
+               public PrepareHttpStc() {
+                       super(bc, HttpService.class, null);
+               }
+
+               @Override
+               public HttpService addingService(ServiceReference<HttpService> reference) {
+                       long begin = System.currentTimeMillis();
+                       if (log.isTraceEnabled())
+                               log.trace("HTTP prepare starts...");
+                       HttpService httpService = addHttpService(reference);
+                       if (log.isTraceEnabled())
+                               log.trace("HTTP prepare duration: " + (System.currentTimeMillis() - begin) + "ms");
+                       return httpService;
+               }
+
+               @Override
+               public void removedService(ServiceReference<HttpService> reference, HttpService service) {
+                       // if (dataHttp != null)
+                       // dataHttp.destroy();
+                       // dataHttp = null;
+                       // if (nodeHttp != null)
+                       // nodeHttp.destroy();
+                       // nodeHttp = null;
+                       // destroy();
+                       repositories.close();
+                       repositories = null;
+               }
+
+               private HttpService addHttpService(ServiceReference<HttpService> sr) {
+                       HttpService httpService = bc.getService(sr);
+                       // TODO find constants
+                       Object httpPort = sr.getProperty("http.port");
+                       Object httpsPort = sr.getProperty("https.port");
+
+                       try {
+                               httpService.registerServlet("/!", new LinkServlet(), null, null);
+                               httpService.registerServlet("/robots.txt", new RobotServlet(), null, null);
+                       } catch (Exception e) {
+                               throw new CmsException("Cannot register filters", e);
+                       }
+                       // track repositories
+                       if (repositories != null)
+                               throw new CmsException("An http service is already configured");
+                       repositories = new RepositoriesStc(bc, httpService);
+                       // repositories.open();
+                       if (cleanState)
+                               KernelUtils.asyncOpen(repositories);
+                       log.info(httpPortsMsg(httpPort, httpsPort));
+                       // httpAvailable = true;
+                       // checkReadiness();
+
+                       bc.registerService(NodeHttp.class, NodeHttp.this, null);
+                       return httpService;
+               }
+
+               private String httpPortsMsg(Object httpPort, Object httpsPort) {
+                       return "HTTP " + httpPort + (httpsPort != null ? " - HTTPS " + httpsPort : "");
+               }
+       }
+
+       private static class WebdavServlet extends SimpleWebdavServlet {
+               private static final long serialVersionUID = -4687354117811443881L;
+               private final Repository repository;
+
+               public WebdavServlet(Repository repository, SessionProvider sessionProvider) {
+                       this.repository = repository;
+                       setSessionProvider(sessionProvider);
+               }
+
+               public Repository getRepository() {
+                       return repository;
+               }
+
+               @Override
+               protected void service(final HttpServletRequest request, final HttpServletResponse response)
+                               throws ServletException, IOException {
+                       WebdavServlet.super.service(request, response);
+                       // try {
+                       // Subject subject = subjectFromRequest(request);
+                       // // TODO make it stronger, with eTags.
+                       // // if (CurrentUser.isAnonymous(subject) &&
+                       // // request.getMethod().equals("GET")) {
+                       // // response.setHeader("Cache-Control", "no-transform, public,
+                       // // max-age=300, s-maxage=900");
+                       // // }
+                       //
+                       // Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
+                       // @Override
+                       // public Void run() throws Exception {
+                       // WebdavServlet.super.service(request, response);
+                       // return null;
+                       // }
+                       // });
+                       // } catch (PrivilegedActionException e) {
+                       // throw new CmsException("Cannot process webdav request",
+                       // e.getException());
+                       // }
+               }
+
+       }
+
+       private static class RemotingServlet extends JcrRemotingServlet {
+               private final Log log = LogFactory.getLog(RemotingServlet.class);
+               private static final long serialVersionUID = 4605238259548058883L;
+               private final Repository repository;
+               private final SessionProvider sessionProvider;
+
+               public RemotingServlet(Repository repository, SessionProvider sessionProvider) {
+                       this.repository = repository;
+                       this.sessionProvider = sessionProvider;
+               }
+
+               @Override
+               protected Repository getRepository() {
+                       return repository;
+               }
+
+               @Override
+               protected SessionProvider getSessionProvider() {
+                       return sessionProvider;
+               }
+
+               @Override
+               protected void service(final HttpServletRequest request, final HttpServletResponse response)
+                               throws ServletException, IOException {
+                       if (log.isTraceEnabled())
+                               HttpUtils.logRequest(log, request);
+                       RemotingServlet.super.service(request, response);
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeKeyRing.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeKeyRing.java
new file mode 100644 (file)
index 0000000..0b774d9
--- /dev/null
@@ -0,0 +1,20 @@
+package org.argeo.cms.internal.kernel;
+
+import java.util.Dictionary;
+
+import javax.jcr.Repository;
+
+import org.argeo.cms.security.JcrKeyring;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+
+class NodeKeyRing extends JcrKeyring implements ManagedService{
+       
+       public NodeKeyRing(Repository repository) {
+               super(repository);
+       }
+
+       @Override
+       public void updated(Dictionary<String, ?> properties) throws ConfigurationException {
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeLogger.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeLogger.java
new file mode 100644 (file)
index 0000000..f7d6df8
--- /dev/null
@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.internal.kernel;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardWatchEventKinds;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.security.SignatureException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PropertyConfigurator;
+import org.apache.log4j.spi.LoggingEvent;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.node.ArgeoLogListener;
+import org.argeo.node.ArgeoLogger;
+import org.argeo.node.NodeConstants;
+import org.argeo.osgi.useradmin.UserAdminConf;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.log.LogEntry;
+import org.osgi.service.log.LogListener;
+import org.osgi.service.log.LogReaderService;
+import org.osgi.service.log.LogService;
+
+/** Not meant to be used directly in standard log4j config */
+class NodeLogger implements ArgeoLogger, LogListener {
+       /** Internal debug for development purposes. */
+       private static Boolean debug = false;
+
+       // private final static Log log = LogFactory.getLog(NodeLogger.class);
+
+       private Boolean disabled = false;
+
+       private String level = null;
+
+       private Level log4jLevel = null;
+       // private Layout layout;
+
+       private Properties configuration;
+
+       private AppenderImpl appender;
+
+       private final List<ArgeoLogListener> everythingListeners = Collections
+                       .synchronizedList(new ArrayList<ArgeoLogListener>());
+       private final List<ArgeoLogListener> allUsersListeners = Collections
+                       .synchronizedList(new ArrayList<ArgeoLogListener>());
+       private final Map<String, List<ArgeoLogListener>> userListeners = Collections
+                       .synchronizedMap(new HashMap<String, List<ArgeoLogListener>>());
+
+       private BlockingQueue<LogEvent> events;
+       private LogDispatcherThread logDispatcherThread = new LogDispatcherThread();
+
+       private Integer maxLastEventsCount = 10 * 1000;
+
+       /** Marker to prevent stack overflow */
+       private ThreadLocal<Boolean> dispatching = new ThreadLocal<Boolean>() {
+
+               @Override
+               protected Boolean initialValue() {
+                       return false;
+               }
+       };
+
+       @SuppressWarnings("unchecked")
+       public NodeLogger(LogReaderService lrs) {
+               Enumeration<LogEntry> logEntries = lrs.getLog();
+               while (logEntries.hasMoreElements())
+                       logged(logEntries.nextElement());
+               lrs.addLogListener(this);
+
+               // configure log4j watcher
+               String log4jConfiguration = KernelUtils.getFrameworkProp("log4j.configuration");
+               if (log4jConfiguration != null && log4jConfiguration.startsWith("file:")) {
+                       if (log4jConfiguration.contains("..")) {
+                               if (log4jConfiguration.startsWith("file://"))
+                                       log4jConfiguration = log4jConfiguration.substring("file://".length());
+                               else if (log4jConfiguration.startsWith("file:"))
+                                       log4jConfiguration = log4jConfiguration.substring("file:".length());
+                       }
+                       try {
+                               Path log4jconfigPath;
+                               if (log4jConfiguration.startsWith("file:"))
+                                       log4jconfigPath = Paths.get(new URI(log4jConfiguration));
+                               else
+                                       log4jconfigPath = Paths.get(log4jConfiguration);
+                               Thread log4jConfWatcher = new Log4jConfWatcherThread(log4jconfigPath);
+                               log4jConfWatcher.start();
+                       } catch (Exception e) {
+                               stdErr("Badly formatted log4j configuration URI " + log4jConfiguration + ": " + e.getMessage());
+                       }
+               }
+       }
+
+       public void init() {
+               try {
+                       events = new LinkedBlockingQueue<LogEvent>();
+
+                       // if (layout != null)
+                       // setLayout(layout);
+                       // else
+                       // setLayout(new PatternLayout(pattern));
+                       appender = new AppenderImpl();
+                       reloadConfiguration();
+                       Logger.getRootLogger().addAppender(appender);
+
+                       logDispatcherThread = new LogDispatcherThread();
+                       logDispatcherThread.start();
+               } catch (Exception e) {
+                       throw new CmsException("Cannot initialize log4j");
+               }
+       }
+
+       public void destroy() throws Exception {
+               Logger.getRootLogger().removeAppender(appender);
+               allUsersListeners.clear();
+               for (List<ArgeoLogListener> lst : userListeners.values())
+                       lst.clear();
+               userListeners.clear();
+
+               events.clear();
+               events = null;
+               logDispatcherThread.interrupt();
+       }
+
+       // public void setLayout(Layout layout) {
+       // this.layout = layout;
+       // }
+
+       public String toString() {
+               return "Node Logger";
+       }
+
+       //
+       // OSGi LOGGER
+       //
+       @Override
+       public void logged(LogEntry status) {
+               Log pluginLog = LogFactory.getLog(status.getBundle().getSymbolicName());
+               Integer severity = status.getLevel();
+               if (severity == LogService.LOG_ERROR) {
+                       // FIXME Fix Argeo TP
+                       if (status.getException() instanceof SignatureException)
+                               return;
+                       pluginLog.error(msg(status), status.getException());
+               } else if (severity == LogService.LOG_WARNING)
+                       pluginLog.warn(msg(status), status.getException());
+               else if (severity == LogService.LOG_INFO && pluginLog.isDebugEnabled())
+                       pluginLog.debug(msg(status), status.getException());
+               else if (severity == LogService.LOG_DEBUG && pluginLog.isTraceEnabled())
+                       pluginLog.trace(msg(status), status.getException());
+       }
+
+       private String msg(LogEntry status) {
+               StringBuilder sb = new StringBuilder();
+               sb.append(status.getMessage());
+               Bundle bundle = status.getBundle();
+               if (bundle != null) {
+                       sb.append(" '" + bundle.getSymbolicName() + "'");
+               }
+               ServiceReference<?> sr = status.getServiceReference();
+               if (sr != null) {
+                       sb.append(' ');
+                       String[] objectClasses = (String[]) sr.getProperty(Constants.OBJECTCLASS);
+                       if (isSpringApplicationContext(objectClasses)) {
+                               sb.append("{org.springframework.context.ApplicationContext}");
+                               Object symbolicName = sr.getProperty(Constants.BUNDLE_SYMBOLICNAME);
+                               if (symbolicName != null)
+                                       sb.append(" " + Constants.BUNDLE_SYMBOLICNAME + ": " + symbolicName);
+                       } else {
+                               sb.append(arrayToString(objectClasses));
+                       }
+                       Object cn = sr.getProperty(NodeConstants.CN);
+                       if (cn != null)
+                               sb.append(" " + NodeConstants.CN + ": " + cn);
+                       Object factoryPid = sr.getProperty(ConfigurationAdmin.SERVICE_FACTORYPID);
+                       if (factoryPid != null)
+                               sb.append(" " + ConfigurationAdmin.SERVICE_FACTORYPID + ": " + factoryPid);
+                       // else {
+                       // Object servicePid = sr.getProperty(Constants.SERVICE_PID);
+                       // if (servicePid != null)
+                       // sb.append(" " + Constants.SERVICE_PID + ": " + servicePid);
+                       // }
+                       // servlets
+                       Object whiteBoardPattern = sr.getProperty(KernelConstants.WHITEBOARD_PATTERN_PROP);
+                       if (whiteBoardPattern != null) {
+                               if (whiteBoardPattern instanceof String) {
+                                       sb.append(" " + KernelConstants.WHITEBOARD_PATTERN_PROP + ": " + whiteBoardPattern);
+                               } else {
+                                       sb.append(" " + KernelConstants.WHITEBOARD_PATTERN_PROP + ": "
+                                                       + arrayToString((String[]) whiteBoardPattern));
+                               }
+                       }
+                       // RWT
+                       Object contextName = sr.getProperty(KernelConstants.CONTEXT_NAME_PROP);
+                       if (contextName != null)
+                               sb.append(" " + KernelConstants.CONTEXT_NAME_PROP + ": " + contextName);
+
+                       // user directories
+                       Object baseDn = sr.getProperty(UserAdminConf.baseDn.name());
+                       if (baseDn != null)
+                               sb.append(" " + UserAdminConf.baseDn.name() + ": " + baseDn);
+
+               }
+               return sb.toString();
+       }
+
+       private String arrayToString(Object[] arr) {
+               StringBuilder sb = new StringBuilder();
+               sb.append('[');
+               for (int i = 0; i < arr.length; i++) {
+                       if (i != 0)
+                               sb.append(',');
+                       sb.append(arr[i]);
+               }
+               sb.append(']');
+               return sb.toString();
+       }
+
+       private boolean isSpringApplicationContext(String[] objectClasses) {
+               for (String clss : objectClasses) {
+                       if (clss.equals("org.eclipse.gemini.blueprint.context.DelegatedExecutionOsgiBundleApplicationContext")) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       //
+       // ARGEO LOGGER
+       //
+
+       public synchronized void register(ArgeoLogListener listener, Integer numberOfPreviousEvents) {
+               String username = CurrentUser.getUsername();
+               if (username == null)
+                       throw new CmsException("Only authenticated users can register a log listener");
+
+               if (!userListeners.containsKey(username)) {
+                       List<ArgeoLogListener> lst = Collections.synchronizedList(new ArrayList<ArgeoLogListener>());
+                       userListeners.put(username, lst);
+               }
+               userListeners.get(username).add(listener);
+               List<LogEvent> lastEvents = logDispatcherThread.getLastEvents(username, numberOfPreviousEvents);
+               for (LogEvent evt : lastEvents)
+                       dispatchEvent(listener, evt);
+       }
+
+       public synchronized void registerForAll(ArgeoLogListener listener, Integer numberOfPreviousEvents,
+                       boolean everything) {
+               if (everything)
+                       everythingListeners.add(listener);
+               else
+                       allUsersListeners.add(listener);
+               List<LogEvent> lastEvents = logDispatcherThread.getLastEvents(null, numberOfPreviousEvents);
+               for (LogEvent evt : lastEvents)
+                       if (everything || evt.getUsername() != null)
+                               dispatchEvent(listener, evt);
+       }
+
+       public synchronized void unregister(ArgeoLogListener listener) {
+               String username = CurrentUser.getUsername();
+               if (username == null)// FIXME
+                       return;
+               if (!userListeners.containsKey(username))
+                       throw new CmsException("No user listeners " + listener + " registered for user " + username);
+               if (!userListeners.get(username).contains(listener))
+                       throw new CmsException("No user listeners " + listener + " registered for user " + username);
+               userListeners.get(username).remove(listener);
+               if (userListeners.get(username).isEmpty())
+                       userListeners.remove(username);
+
+       }
+
+       public synchronized void unregisterForAll(ArgeoLogListener listener) {
+               everythingListeners.remove(listener);
+               allUsersListeners.remove(listener);
+       }
+
+       /** For development purpose, since using regular logging is not easy here */
+       private static void stdOut(Object obj) {
+               System.out.println(obj);
+       }
+
+       private static void stdErr(Object obj) {
+               System.err.println(obj);
+       }
+
+       private static void debug(Object obj) {
+               if (debug)
+                       System.out.println(obj);
+       }
+
+       private static boolean isInternalDebugEnabled() {
+               return debug;
+       }
+
+       // public void setPattern(String pattern) {
+       // this.pattern = pattern;
+       // }
+
+       public void setDisabled(Boolean disabled) {
+               this.disabled = disabled;
+       }
+
+       public void setLevel(String level) {
+               this.level = level;
+       }
+
+       public void setConfiguration(Properties configuration) {
+               this.configuration = configuration;
+       }
+
+       public void updateConfiguration(Properties configuration) {
+               setConfiguration(configuration);
+               reloadConfiguration();
+       }
+
+       public Properties getConfiguration() {
+               return configuration;
+       }
+
+       /**
+        * Reloads configuration (if the configuration {@link Properties} is set)
+        */
+       protected void reloadConfiguration() {
+               if (configuration != null) {
+                       LogManager.resetConfiguration();
+                       PropertyConfigurator.configure(configuration);
+               }
+       }
+
+       protected synchronized void processLoggingEvent(LogEvent event) {
+               if (disabled)
+                       return;
+
+               if (dispatching.get())
+                       return;
+
+               if (level != null && !level.trim().equals("")) {
+                       if (log4jLevel == null || !log4jLevel.toString().equals(level))
+                               try {
+                                       log4jLevel = Level.toLevel(level);
+                               } catch (Exception e) {
+                                       System.err.println("Log4j level could not be set for level '" + level + "', resetting it to null.");
+                                       e.printStackTrace();
+                                       level = null;
+                               }
+
+                       if (log4jLevel != null && !event.getLoggingEvent().getLevel().isGreaterOrEqual(log4jLevel)) {
+                               return;
+                       }
+               }
+
+               try {
+                       // admin listeners
+                       Iterator<ArgeoLogListener> everythingIt = everythingListeners.iterator();
+                       while (everythingIt.hasNext())
+                               dispatchEvent(everythingIt.next(), event);
+
+                       if (event.getUsername() != null) {
+                               Iterator<ArgeoLogListener> allUsersIt = allUsersListeners.iterator();
+                               while (allUsersIt.hasNext())
+                                       dispatchEvent(allUsersIt.next(), event);
+
+                               if (userListeners.containsKey(event.getUsername())) {
+                                       Iterator<ArgeoLogListener> userIt = userListeners.get(event.getUsername()).iterator();
+                                       while (userIt.hasNext())
+                                               dispatchEvent(userIt.next(), event);
+                               }
+                       }
+               } catch (Exception e) {
+                       stdOut("Cannot process logging event");
+                       e.printStackTrace();
+               }
+       }
+
+       protected void dispatchEvent(ArgeoLogListener logListener, LogEvent evt) {
+               LoggingEvent event = evt.getLoggingEvent();
+               logListener.appendLog(evt.getUsername(), event.getTimeStamp(), event.getLevel().toString(),
+                               event.getLoggerName(), event.getThreadName(), event.getMessage(), event.getThrowableStrRep());
+       }
+
+       private class AppenderImpl extends AppenderSkeleton {
+               public boolean requiresLayout() {
+                       return false;
+               }
+
+               public void close() {
+               }
+
+               @Override
+               protected void append(LoggingEvent event) {
+                       if (events != null) {
+                               try {
+                                       String username = CurrentUser.getUsername();
+                                       events.put(new LogEvent(username, event));
+                               } catch (InterruptedException e) {
+                                       // silent
+                               }
+                       }
+               }
+
+       }
+
+       private class LogDispatcherThread extends Thread {
+               /** encapsulated in order to simplify concurrency management */
+               private LinkedList<LogEvent> lastEvents = new LinkedList<LogEvent>();
+
+               public LogDispatcherThread() {
+                       super("Argeo Logging Dispatcher Thread");
+               }
+
+               public void run() {
+                       while (events != null) {
+                               try {
+                                       LogEvent loggingEvent = events.take();
+                                       processLoggingEvent(loggingEvent);
+                                       addLastEvent(loggingEvent);
+                               } catch (InterruptedException e) {
+                                       if (events == null)
+                                               return;
+                               }
+                       }
+               }
+
+               protected synchronized void addLastEvent(LogEvent loggingEvent) {
+                       if (lastEvents.size() >= maxLastEventsCount)
+                               lastEvents.poll();
+                       lastEvents.add(loggingEvent);
+               }
+
+               public synchronized List<LogEvent> getLastEvents(String username, Integer maxCount) {
+                       LinkedList<LogEvent> evts = new LinkedList<LogEvent>();
+                       ListIterator<LogEvent> it = lastEvents.listIterator(lastEvents.size());
+                       int count = 0;
+                       while (it.hasPrevious() && (count < maxCount)) {
+                               LogEvent evt = it.previous();
+                               if (username == null || username.equals(evt.getUsername())) {
+                                       evts.push(evt);
+                                       count++;
+                               }
+                       }
+                       return evts;
+               }
+       }
+
+       private class LogEvent {
+               private final String username;
+               private final LoggingEvent loggingEvent;
+
+               public LogEvent(String username, LoggingEvent loggingEvent) {
+                       super();
+                       this.username = username;
+                       this.loggingEvent = loggingEvent;
+               }
+
+               @Override
+               public int hashCode() {
+                       return loggingEvent.hashCode();
+               }
+
+               @Override
+               public boolean equals(Object obj) {
+                       return loggingEvent.equals(obj);
+               }
+
+               @Override
+               public String toString() {
+                       return username + "@ " + loggingEvent.toString();
+               }
+
+               public String getUsername() {
+                       return username;
+               }
+
+               public LoggingEvent getLoggingEvent() {
+                       return loggingEvent;
+               }
+
+       }
+
+       private class Log4jConfWatcherThread extends Thread {
+               private Path log4jConfigurationPath;
+
+               public Log4jConfWatcherThread(Path log4jConfigurationPath) {
+                       super("Log4j Configuration Watcher");
+                       try {
+                               this.log4jConfigurationPath = log4jConfigurationPath.toRealPath();
+                       } catch (IOException e) {
+                               this.log4jConfigurationPath = log4jConfigurationPath.toAbsolutePath();
+                               stdOut("Cannot determine real path for " + log4jConfigurationPath + ": " + e.getMessage());
+                       }
+               }
+
+               public void run() {
+                       Path parentDir = log4jConfigurationPath.getParent();
+                       try (final WatchService watchService = FileSystems.getDefault().newWatchService()) {
+                               parentDir.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
+                               WatchKey wk;
+                               watching: while ((wk = watchService.take()) != null) {
+                                       for (WatchEvent<?> event : wk.pollEvents()) {
+                                               final Path changed = (Path) event.context();
+                                               if (log4jConfigurationPath.equals(parentDir.resolve(changed))) {
+                                                       if (isInternalDebugEnabled())
+                                                               debug(log4jConfigurationPath + " has changed, reloading.");
+                                                       PropertyConfigurator.configure(log4jConfigurationPath.toUri().toURL());
+                                               }
+                                       }
+                                       // reset the key
+                                       boolean valid = wk.reset();
+                                       if (!valid) {
+                                               break watching;
+                                       }
+                               }
+                       } catch (IOException | InterruptedException e) {
+                               stdErr("Log4j configuration watcher failed: " + e.getMessage());
+                       }
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeRepositoryFactory.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeRepositoryFactory.java
new file mode 100644 (file)
index 0000000..f83eb94
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.internal.kernel;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory;
+import org.argeo.cms.internal.jcr.RepoConf;
+import org.argeo.jcr.ArgeoJcrException;
+import org.argeo.node.NodeConstants;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.springframework.core.io.Resource;
+
+/**
+ * OSGi-aware Jackrabbit repository factory which can retrieve/publish
+ * {@link Repository} as OSGi services.
+ */
+class NodeRepositoryFactory implements RepositoryFactory {
+       private final Log log = LogFactory.getLog(getClass());
+       private final BundleContext bundleContext = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+       // private Resource fileRepositoryConfiguration = new ClassPathResource(
+       // "/org/argeo/cms/internal/kernel/repository-localfs.xml");
+
+       protected Repository getRepositoryByAlias(String alias) {
+               try {
+                       Collection<ServiceReference<Repository>> srs = bundleContext.getServiceReferences(Repository.class,
+                                       "(" + NodeConstants.CN + "=" + alias + ")");
+                       if (srs.size() == 0)
+                               throw new ArgeoJcrException("No repository with alias " + alias + " found in OSGi registry");
+                       else if (srs.size() > 1)
+                               throw new ArgeoJcrException(
+                                               srs.size() + " repositories with alias " + alias + " found in OSGi registry");
+                       return bundleContext.getService(srs.iterator().next());
+               } catch (InvalidSyntaxException e) {
+                       throw new ArgeoJcrException("Cannot find repository with alias " + alias, e);
+               }
+       }
+
+       // private void publish(String alias, Repository repository, Properties
+       // properties) {
+       // if (bundleContext != null) {
+       // // do not modify reference
+       // Hashtable<String, String> props = new Hashtable<String, String>();
+       // props.putAll(props);
+       // props.put(JCR_REPOSITORY_ALIAS, alias);
+       // bundleContext.registerService(Repository.class.getName(), repository,
+       // props);
+       // }
+       // }
+
+       @SuppressWarnings({ "rawtypes" })
+       public Repository getRepository(Map parameters) throws RepositoryException {
+               // // check if can be found by alias
+               // Repository repository = super.getRepository(parameters);
+               // if (repository != null)
+               // return repository;
+
+               // check if remote
+               Repository repository;
+               String uri = null;
+               if (parameters.containsKey(RepoConf.labeledUri.name()))
+                       uri = parameters.get(NodeConstants.LABELED_URI).toString();
+               else if (parameters.containsKey(KernelConstants.JACKRABBIT_REPOSITORY_URI))
+                       uri = parameters.get(KernelConstants.JACKRABBIT_REPOSITORY_URI).toString();
+
+               if (uri != null) {
+                       if (uri.startsWith("http")) {// http, https
+                               Object defaultWorkspace = parameters.get(RepoConf.defaultWorkspace.name());
+                               repository = createRemoteRepository(uri, defaultWorkspace != null ? defaultWorkspace.toString() : null);
+                       } else if (uri.startsWith("file"))// http, https
+                               repository = createFileRepository(uri, parameters);
+                       else if (uri.startsWith("vm")) {
+                               // log.warn("URI " + uri + " should have been managed by generic
+                               // JCR repository factory");
+                               repository = getRepositoryByAlias(getAliasFromURI(uri));
+                       } else
+                               throw new ArgeoJcrException("Unrecognized URI format " + uri);
+
+               }
+
+               else if (parameters.containsKey(NodeConstants.CN)) {
+                       // Properties properties = new Properties();
+                       // properties.putAll(parameters);
+                       String alias = parameters.get(NodeConstants.CN).toString();
+                       // publish(alias, repository, properties);
+                       // log.info("Registered JCR repository under alias '" + alias + "'
+                       // with properties " + properties);
+                       repository = getRepositoryByAlias(alias);
+               } else
+                       throw new ArgeoJcrException("Not enough information in " + parameters);
+
+               if (repository == null)
+                       throw new ArgeoJcrException("Repository not found " + parameters);
+
+               return repository;
+       }
+
+       protected Repository createRemoteRepository(String uri, String defaultWorkspace) throws RepositoryException {
+               Map<String, String> params = new HashMap<String, String>();
+               params.put(KernelConstants.JACKRABBIT_REPOSITORY_URI, uri);
+               if (defaultWorkspace != null)
+                       params.put(KernelConstants.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, defaultWorkspace);
+               Repository repository = new Jcr2davRepositoryFactory().getRepository(params);
+               if (repository == null)
+                       throw new ArgeoJcrException("Remote Davex repository " + uri + " not found");
+               log.info("Initialized remote Jackrabbit repository from uri " + uri);
+               return repository;
+       }
+
+       @SuppressWarnings({ "rawtypes" })
+       protected Repository createFileRepository(final String uri, Map parameters) throws RepositoryException {
+               throw new UnsupportedOperationException();
+               // InputStream configurationIn = null;
+               // try {
+               // Properties vars = new Properties();
+               // vars.putAll(parameters);
+               // String dirPath = uri.substring("file:".length());
+               // File homeDir = new File(dirPath);
+               // if (homeDir.exists() && !homeDir.isDirectory())
+               // throw new ArgeoJcrException("Repository home " + dirPath + " is not a
+               // directory");
+               // if (!homeDir.exists())
+               // homeDir.mkdirs();
+               // configurationIn = fileRepositoryConfiguration.getInputStream();
+               // vars.put(RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE,
+               // homeDir.getCanonicalPath());
+               // RepositoryConfig repositoryConfig = RepositoryConfig.create(new
+               // InputSource(configurationIn), vars);
+               //
+               // // TransientRepository repository = new
+               // // TransientRepository(repositoryConfig);
+               // final RepositoryImpl repository =
+               // RepositoryImpl.create(repositoryConfig);
+               // Session session = repository.login();
+               // // FIXME make it generic
+               // org.argeo.jcr.JcrUtils.addPrivilege(session, "/", "ROLE_ADMIN",
+               // "jcr:all");
+               // org.argeo.jcr.JcrUtils.logoutQuietly(session);
+               // Runtime.getRuntime().addShutdownHook(new Thread("Clean JCR repository
+               // " + uri) {
+               // public void run() {
+               // repository.shutdown();
+               // log.info("Destroyed repository " + uri);
+               // }
+               // });
+               // log.info("Initialized file Jackrabbit repository from uri " + uri);
+               // return repository;
+               // } catch (Exception e) {
+               // throw new ArgeoJcrException("Cannot create repository " + uri, e);
+               // } finally {
+               // IOUtils.closeQuietly(configurationIn);
+               // }
+       }
+
+       protected String getAliasFromURI(String uri) {
+               try {
+                       URI uriObj = new URI(uri);
+                       String alias = uriObj.getPath();
+                       if (alias.charAt(0) == '/')
+                               alias = alias.substring(1);
+                       if (alias.charAt(alias.length() - 1) == '/')
+                               alias = alias.substring(0, alias.length() - 1);
+                       return alias;
+               } catch (URISyntaxException e) {
+                       throw new ArgeoJcrException("Cannot interpret URI " + uri, e);
+               }
+       }
+
+       /**
+        * Called after the repository has been initialised. Does nothing by default.
+        */
+       @SuppressWarnings("rawtypes")
+       protected void postInitialization(Repository repository, Map parameters) {
+
+       }
+
+       public void setFileRepositoryConfiguration(Resource fileRepositoryConfiguration) {
+               // this.fileRepositoryConfiguration = fileRepositoryConfiguration;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeUserAdmin.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/NodeUserAdmin.java
new file mode 100644 (file)
index 0000000..e8977fa
--- /dev/null
@@ -0,0 +1,316 @@
+package org.argeo.cms.internal.kernel;
+
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.ldap.LdapName;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.kerberos.KerberosPrincipal;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+import javax.transaction.TransactionManager;
+
+import org.apache.commons.httpclient.auth.AuthPolicy;
+import org.apache.commons.httpclient.auth.CredentialsProvider;
+import org.apache.commons.httpclient.params.DefaultHttpParams;
+import org.apache.commons.httpclient.params.HttpMethodParams;
+import org.apache.commons.httpclient.params.HttpParams;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.internal.http.client.HttpCredentialProvider;
+import org.argeo.cms.internal.http.client.SpnegoAuthScheme;
+import org.argeo.naming.DnsBrowser;
+import org.argeo.node.NodeConstants;
+import org.argeo.osgi.useradmin.AbstractUserDirectory;
+import org.argeo.osgi.useradmin.AggregatingUserAdmin;
+import org.argeo.osgi.useradmin.LdapUserAdmin;
+import org.argeo.osgi.useradmin.LdifUserAdmin;
+import org.argeo.osgi.useradmin.OsUserDirectory;
+import org.argeo.osgi.useradmin.UserAdminConf;
+import org.argeo.osgi.useradmin.UserDirectory;
+import org.ietf.jgss.GSSCredential;
+import org.ietf.jgss.GSSException;
+import org.ietf.jgss.GSSManager;
+import org.ietf.jgss.GSSName;
+import org.ietf.jgss.Oid;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedServiceFactory;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+
+import bitronix.tm.BitronixTransactionManager;
+import bitronix.tm.resource.ehcache.EhCacheXAResourceProducer;
+
+/**
+ * Aggregates multiple {@link UserDirectory} and integrates them with system
+ * roles.
+ */
+class NodeUserAdmin extends AggregatingUserAdmin implements ManagedServiceFactory, KernelConstants {
+       private final static Log log = LogFactory.getLog(NodeUserAdmin.class);
+       private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext();
+
+       // OSGi
+       private Map<String, LdapName> pidToBaseDn = new HashMap<>();
+       private Map<String, ServiceRegistration<UserDirectory>> pidToServiceRegs = new HashMap<>();
+       private ServiceRegistration<UserAdmin> userAdminReg;
+
+       // JTA
+       private final ServiceTracker<TransactionManager, TransactionManager> tmTracker;
+       private final String cacheName = UserDirectory.class.getName();
+
+       // GSS API
+       private Path nodeKeyTab = KernelUtils.getOsgiInstancePath(KernelConstants.NODE_KEY_TAB_PATH);
+       private GSSCredential acceptorCredentials;
+
+       private boolean singleUser = false;
+       private boolean systemRolesAvailable = false;
+
+       public NodeUserAdmin(String systemRolesBaseDn) {
+               super(systemRolesBaseDn);
+               tmTracker = new ServiceTracker<>(bc, TransactionManager.class, null);
+               tmTracker.open();
+       }
+
+       @Override
+       public void updated(String pid, Dictionary<String, ?> properties) throws ConfigurationException {
+               String uri = (String) properties.get(UserAdminConf.uri.name());
+               URI u;
+               try {
+                       if (uri == null) {
+                               String baseDn = (String) properties.get(UserAdminConf.baseDn.name());
+                               u = KernelUtils.getOsgiInstanceUri(KernelConstants.DIR_NODE + '/' + baseDn + ".ldif");
+                       } else
+                               u = new URI(uri);
+               } catch (URISyntaxException e) {
+                       throw new CmsException("Badly formatted URI " + uri, e);
+               }
+
+               // Create
+               AbstractUserDirectory userDirectory;
+               if (UserAdminConf.SCHEME_LDAP.equals(u.getScheme())) {
+                       userDirectory = new LdapUserAdmin(properties);
+               } else if (UserAdminConf.SCHEME_FILE.equals(u.getScheme())) {
+                       userDirectory = new LdifUserAdmin(u, properties);
+               } else if (UserAdminConf.SCHEME_OS.equals(u.getScheme())) {
+                       userDirectory = new OsUserDirectory(u, properties);
+                       singleUser = true;
+               } else {
+                       throw new CmsException("Unsupported scheme " + u.getScheme());
+               }
+               Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
+               addUserDirectory(userDirectory);
+
+               // OSGi
+               LdapName baseDn = userDirectory.getBaseDn();
+               Dictionary<String, Object> regProps = new Hashtable<>();
+               regProps.put(Constants.SERVICE_PID, pid);
+               if (isSystemRolesBaseDn(baseDn))
+                       regProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
+               regProps.put(UserAdminConf.baseDn.name(), baseDn);
+               ServiceRegistration<UserDirectory> reg = bc.registerService(UserDirectory.class, userDirectory, regProps);
+               pidToBaseDn.put(pid, baseDn);
+               pidToServiceRegs.put(pid, reg);
+
+               if (log.isDebugEnabled())
+                       log.debug("User directory " + userDirectory.getBaseDn() + " [" + u.getScheme() + "] enabled."
+                                       + (realm != null ? " " + realm + " realm." : ""));
+
+               if (isSystemRolesBaseDn(baseDn))
+                       systemRolesAvailable = true;
+
+               // start publishing only when system roles are available
+               if (systemRolesAvailable) {
+                       // The list of baseDns is published as properties
+                       // TODO clients should rather reference USerDirectory services
+                       if (userAdminReg != null)
+                               userAdminReg.unregister();
+                       // register self as main user admin
+                       Dictionary<String, Object> userAdminregProps = currentState();
+                       userAdminregProps.put(NodeConstants.CN, NodeConstants.DEFAULT);
+                       userAdminregProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE);
+                       userAdminReg = bc.registerService(UserAdmin.class, this, userAdminregProps);
+               }
+       }
+
+       @Override
+       public void deleted(String pid) {
+               assert pidToServiceRegs.get(pid) != null;
+               assert pidToBaseDn.get(pid) != null;
+               pidToServiceRegs.remove(pid).unregister();
+               LdapName baseDn = pidToBaseDn.remove(pid);
+               removeUserDirectory(baseDn);
+       }
+
+       @Override
+       public String getName() {
+               return "Node User Admin";
+       }
+
+       @Override
+       protected void addAbstractSystemRoles(Authorization rawAuthorization, Set<String> sysRoles) {
+               if (rawAuthorization.getName() == null) {
+                       sysRoles.add(NodeConstants.ROLE_ANONYMOUS);
+               } else {
+                       sysRoles.add(NodeConstants.ROLE_USER);
+               }
+       }
+
+       protected void postAdd(AbstractUserDirectory userDirectory) {
+               // JTA
+               TransactionManager tm = tmTracker.getService();
+               if (tm == null)
+                       throw new CmsException("A JTA transaction manager must be available.");
+               userDirectory.setTransactionManager(tm);
+               if (tmTracker.getService() instanceof BitronixTransactionManager)
+                       EhCacheXAResourceProducer.registerXAResource(cacheName, userDirectory.getXaResource());
+
+               Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
+               if (realm != null) {
+                       if (Files.exists(nodeKeyTab)) {
+                               String servicePrincipal = getKerberosServicePrincipal(realm.toString());
+                               if (servicePrincipal != null) {
+                                       CallbackHandler callbackHandler = new CallbackHandler() {
+                                               @Override
+                                               public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+                                                       for (Callback callback : callbacks)
+                                                               if (callback instanceof NameCallback)
+                                                                       ((NameCallback) callback).setName(servicePrincipal);
+
+                                               }
+                                       };
+                                       try {
+                                               LoginContext nodeLc = new LoginContext(NodeConstants.LOGIN_CONTEXT_NODE, callbackHandler);
+                                               nodeLc.login();
+                                               acceptorCredentials = logInAsAcceptor(nodeLc.getSubject(), servicePrincipal);
+                                       } catch (LoginException e) {
+                                               throw new CmsException("Cannot log in kernel", e);
+                                       }
+                               }
+                       }
+
+                       // Register client-side SPNEGO auth scheme
+                       AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class);
+                       HttpParams params = DefaultHttpParams.getDefaultParams();
+                       ArrayList<String> schemes = new ArrayList<>();
+                       schemes.add(SpnegoAuthScheme.NAME);// SPNEGO preferred
+                       // schemes.add(AuthPolicy.BASIC);// incompatible with Basic
+                       params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes);
+                       params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider());
+                       params.setParameter(HttpMethodParams.COOKIE_POLICY, KernelConstants.COOKIE_POLICY_BROWSER_COMPATIBILITY);
+                       // params.setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
+               }
+       }
+
+       protected void preDestroy(AbstractUserDirectory userDirectory) {
+               if (tmTracker.getService() instanceof BitronixTransactionManager)
+                       EhCacheXAResourceProducer.unregisterXAResource(cacheName, userDirectory.getXaResource());
+
+               Object realm = userDirectory.getProperties().get(UserAdminConf.realm.name());
+               if (realm != null) {
+                       if (acceptorCredentials != null) {
+                               try {
+                                       acceptorCredentials.dispose();
+                               } catch (GSSException e) {
+                                       // silent
+                               }
+                               acceptorCredentials = null;
+                       }
+               }
+       }
+
+       private String getKerberosServicePrincipal(String realm) {
+               String hostname;
+               try (DnsBrowser dnsBrowser = new DnsBrowser()) {
+                       InetAddress localhost = InetAddress.getLocalHost();
+                       hostname = localhost.getHostName();
+                       String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
+                       String ipfromDns = dnsBrowser.getRecord(hostname, localhost instanceof Inet6Address ? "AAAA" : "A");
+                       boolean consistentIp = localhost.getHostAddress().equals(ipfromDns);
+                       String kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
+                       if (consistentIp && kerberosDomain != null && kerberosDomain.equals(realm) && Files.exists(nodeKeyTab)) {
+                               return NodeHttp.DEFAULT_SERVICE + "/" + hostname + "@" + kerberosDomain;
+                       } else
+                               return null;
+               } catch (Exception e) {
+                       log.warn("Exception when determining kerberos principal", e);
+                       return null;
+               }
+       }
+
+       private GSSCredential logInAsAcceptor(Subject subject, String servicePrincipal) {
+               // GSS
+               Iterator<KerberosPrincipal> krb5It = subject.getPrincipals(KerberosPrincipal.class).iterator();
+               if (!krb5It.hasNext())
+                       return null;
+               KerberosPrincipal krb5Principal = null;
+               while (krb5It.hasNext()) {
+                       KerberosPrincipal principal = krb5It.next();
+                       if (principal.getName().equals(servicePrincipal))
+                               krb5Principal = principal;
+               }
+
+               if (krb5Principal == null)
+                       return null;
+
+               GSSManager manager = GSSManager.getInstance();
+               try {
+                       GSSName gssName = manager.createName(krb5Principal.getName(), null);
+                       GSSCredential serverCredentials = Subject.doAs(subject, new PrivilegedExceptionAction<GSSCredential>() {
+
+                               @Override
+                               public GSSCredential run() throws GSSException {
+                                       return manager.createCredential(gssName, GSSCredential.INDEFINITE_LIFETIME, KERBEROS_OID,
+                                                       GSSCredential.ACCEPT_ONLY);
+
+                               }
+
+                       });
+                       if (log.isDebugEnabled())
+                               log.debug("GSS acceptor configured for " + krb5Principal);
+                       return serverCredentials;
+               } catch (Exception gsse) {
+                       throw new CmsException("Cannot create acceptor credentials for " + krb5Principal, gsse);
+               }
+       }
+
+       public GSSCredential getAcceptorCredentials() {
+               return acceptorCredentials;
+       }
+
+       public boolean isSingleUser() {
+               return singleUser;
+       }
+
+       public final static Oid KERBEROS_OID;
+       static {
+               try {
+                       KERBEROS_OID = new Oid("1.3.6.1.5.5.2");
+               } catch (GSSException e) {
+                       throw new IllegalStateException("Cannot create Kerberos OID", e);
+               }
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/PkiUtils.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/PkiUtils.java
new file mode 100644 (file)
index 0000000..dbd0456
--- /dev/null
@@ -0,0 +1,163 @@
+package org.argeo.cms.internal.kernel;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.argeo.cms.CmsException;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+/**
+ * Utilities around private keys and certificate, mostly wrapping BouncyCastle
+ * implementations.
+ */
+class PkiUtils {
+       private final static String SECURITY_PROVIDER;
+       static {
+               Security.addProvider(new BouncyCastleProvider());
+               SECURITY_PROVIDER = "BC";
+       }
+
+       public static X509Certificate generateSelfSignedCertificate(KeyStore keyStore, X500Principal x500Principal,
+                       int keySize, char[] keyPassword) {
+               try {
+                       KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", SECURITY_PROVIDER);
+                       kpGen.initialize(keySize, new SecureRandom());
+                       KeyPair pair = kpGen.generateKeyPair();
+                       Date notBefore = new Date(System.currentTimeMillis() - 10000);
+                       Date notAfter = new Date(System.currentTimeMillis() + 365 * 24L * 3600 * 1000);
+                       BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
+                       X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(x500Principal, serial, notBefore,
+                                       notAfter, x500Principal, pair.getPublic());
+                       ContentSigner sigGen = new JcaContentSignerBuilder("SHA256WithRSAEncryption").setProvider(SECURITY_PROVIDER)
+                                       .build(pair.getPrivate());
+                       X509Certificate cert = new JcaX509CertificateConverter().setProvider(SECURITY_PROVIDER)
+                                       .getCertificate(certGen.build(sigGen));
+                       cert.checkValidity(new Date());
+                       cert.verify(cert.getPublicKey());
+
+                       keyStore.setKeyEntry(x500Principal.getName(), pair.getPrivate(), keyPassword, new Certificate[] { cert });
+                       return cert;
+               } catch (Exception e) {
+                       throw new CmsException("Cannot generate self-signed certificate", e);
+               }
+       }
+
+       public static KeyStore getKeyStore(File keyStoreFile, char[] keyStorePassword) {
+               try {
+                       KeyStore store = KeyStore.getInstance("JKS", SECURITY_PROVIDER);
+                       if (keyStoreFile.exists()) {
+                               try (FileInputStream fis = new FileInputStream(keyStoreFile)) {
+                                       store.load(fis, keyStorePassword);
+                               }
+                       } else {
+                               store.load(null);
+                       }
+                       return store;
+               } catch (Exception e) {
+                       throw new CmsException("Cannot load keystore " + keyStoreFile, e);
+               }
+       }
+
+       public static void saveKeyStore(File keyStoreFile, char[] keyStorePassword, KeyStore keyStore) {
+               try {
+                       try (FileOutputStream fis = new FileOutputStream(keyStoreFile)) {
+                               keyStore.store(fis, keyStorePassword);
+                       }
+               } catch (Exception e) {
+                       throw new CmsException("Cannot save keystore " + keyStoreFile, e);
+               }
+       }
+
+       public static void main(String[] args) {
+               final String ALGORITHM = "RSA";
+               final String provider = "BC";
+               SecureRandom secureRandom = new SecureRandom();
+               long begin = System.currentTimeMillis();
+               for (int i = 512; i < 1024; i = i + 2) {
+                       try {
+                               KeyPairGenerator keyGen = KeyPairGenerator.getInstance(ALGORITHM, provider);
+                               keyGen.initialize(i, secureRandom);
+                               keyGen.generateKeyPair();
+                       } catch (Exception e) {
+                               System.err.println(i + " : " + e.getMessage());
+                       }
+               }
+               System.out.println((System.currentTimeMillis() - begin) + " ms");
+
+               // // String text = "a";
+               // String text =
+               // "testtesttesttesttesttesttesttesttesttesttesttesttesttesttest";
+               // try {
+               // System.out.println(text);
+               // PrivateKey privateKey;
+               // PublicKey publicKey;
+               // char[] password = "changeit".toCharArray();
+               // String alias = "CN=test";
+               // KeyStore keyStore = KeyStore.getInstance("pkcs12");
+               // File p12file = new File("test.p12");
+               // p12file.delete();
+               // if (!p12file.exists()) {
+               // keyStore.load(null);
+               // generateSelfSignedCertificate(keyStore, new X500Principal(alias),
+               // 513, password);
+               // try (OutputStream out = new FileOutputStream(p12file)) {
+               // keyStore.store(out, password);
+               // }
+               // }
+               // try (InputStream in = new FileInputStream(p12file)) {
+               // keyStore.load(in, password);
+               // privateKey = (PrivateKey) keyStore.getKey(alias, password);
+               // publicKey = keyStore.getCertificateChain(alias)[0].getPublicKey();
+               // }
+               // // KeyPair key;
+               // // final KeyPairGenerator keyGen =
+               // // KeyPairGenerator.getInstance(ALGORITHM);
+               // // keyGen.initialize(4096, new SecureRandom());
+               // // long begin = System.currentTimeMillis();
+               // // key = keyGen.generateKeyPair();
+               // // System.out.println((System.currentTimeMillis() - begin) + " ms");
+               // // keyStore.load(null);
+               // // keyStore.setKeyEntry("test", key.getPrivate(), password, null);
+               // // try(OutputStream out=new FileOutputStream(p12file)) {
+               // // keyStore.store(out, password);
+               // // }
+               // // privateKey = key.getPrivate();
+               // // publicKey = key.getPublic();
+               //
+               // Cipher encrypt = Cipher.getInstance(ALGORITHM);
+               // encrypt.init(Cipher.ENCRYPT_MODE, publicKey);
+               // byte[] encrypted = encrypt.doFinal(text.getBytes());
+               // String encryptedBase64 =
+               // Base64.getEncoder().encodeToString(encrypted);
+               // System.out.println(encryptedBase64);
+               // byte[] encryptedFromBase64 =
+               // Base64.getDecoder().decode(encryptedBase64);
+               //
+               // Cipher decrypt = Cipher.getInstance(ALGORITHM);
+               // decrypt.init(Cipher.DECRYPT_MODE, privateKey);
+               // byte[] decrypted = decrypt.doFinal(encryptedFromBase64);
+               // System.out.println(new String(decrypted));
+               // } catch (Exception e) {
+               // e.printStackTrace();
+               // }
+
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryServiceFactory.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/RepositoryServiceFactory.java
new file mode 100644 (file)
index 0000000..97a3e8d
--- /dev/null
@@ -0,0 +1,130 @@
+package org.argeo.cms.internal.kernel;
+
+import java.net.URI;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryFactory;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.core.RepositoryContext;
+import org.argeo.cms.CmsException;
+import org.argeo.cms.internal.jcr.RepoConf;
+import org.argeo.cms.internal.jcr.RepositoryBuilder;
+import org.argeo.node.NodeConstants;
+import org.argeo.util.LangUtils;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedServiceFactory;
+
+class RepositoryServiceFactory implements ManagedServiceFactory {
+       private final static Log log = LogFactory.getLog(RepositoryServiceFactory.class);
+       private final BundleContext bc = FrameworkUtil.getBundle(RepositoryServiceFactory.class).getBundleContext();
+
+       private Map<String, RepositoryContext> repositories = new HashMap<String, RepositoryContext>();
+       private Map<String, Object> pidToCn = new HashMap<String, Object>();
+
+       @Override
+       public String getName() {
+               return "Jackrabbit repository service factory";
+       }
+
+       @Override
+       public void updated(String pid, Dictionary<String, ?> properties) throws ConfigurationException {
+               if (repositories.containsKey(pid))
+                       throw new CmsException("Already a repository registered for " + pid);
+
+               if (properties == null)
+                       return;
+
+               if (repositories.containsKey(pid)) {
+                       log.warn("Ignore update of Jackrabbit repository " + pid);
+                       return;
+               }
+
+               try {
+                       Object labeledUri = properties.get(RepoConf.labeledUri.name());
+                       if (labeledUri == null) {
+                               RepositoryBuilder repositoryBuilder = new RepositoryBuilder();
+                               RepositoryContext repositoryContext = repositoryBuilder.createRepositoryContext(properties);
+                               repositories.put(pid, repositoryContext);
+                               Dictionary<String, Object> props = LangUtils.dico(Constants.SERVICE_PID, pid);
+                               // props.put(ArgeoJcrConstants.JCR_REPOSITORY_URI,
+                               // properties.get(RepoConf.labeledUri.name()));
+                               Object cn = properties.get(NodeConstants.CN);
+                               if (cn != null) {
+                                       props.put(NodeConstants.CN, cn);
+                                       // props.put(NodeConstants.JCR_REPOSITORY_ALIAS, cn);
+                                       pidToCn.put(pid, cn);
+                               }
+                               bc.registerService(RepositoryContext.class, repositoryContext, props);
+                       } else {
+                               try {
+                                       Object cn = properties.get(NodeConstants.CN);
+                                       Object defaultWorkspace = properties.get(RepoConf.defaultWorkspace.name());
+                                       if (defaultWorkspace == null)
+                                               defaultWorkspace = RepoConf.defaultWorkspace.getDefault();
+                                       URI uri = new URI(labeledUri.toString());
+                                       RepositoryFactory repositoryFactory = bc
+                                                       .getService(bc.getServiceReference(RepositoryFactory.class));
+                                       Map<String, String> parameters = new HashMap<String, String>();
+                                       parameters.put(RepoConf.labeledUri.name(), uri.toString());
+                                       parameters.put(RepoConf.defaultWorkspace.name(), defaultWorkspace.toString());
+                                       Repository repository = repositoryFactory.getRepository(parameters);
+                                       // Repository repository = NodeUtils.getRepositoryByUri(repositoryFactory,
+                                       // uri.toString());
+                                       Dictionary<String, Object> props = LangUtils.dico(Constants.SERVICE_PID, pid);
+                                       props.put(RepoConf.labeledUri.name(),
+                                                       new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null)
+                                                                       .toString());
+                                       if (cn != null) {
+                                               props.put(NodeConstants.CN, cn);
+                                               // props.put(NodeConstants.JCR_REPOSITORY_ALIAS, cn);
+                                               pidToCn.put(pid, cn);
+                                       }
+                                       bc.registerService(Repository.class, repository, props);
+
+                                       // home
+                                       // TODO make a sperate home configurable
+                                       if (cn.equals(NodeConstants.NODE)) {
+                                               Dictionary<String, Object> homeProps = LangUtils.dico(NodeConstants.CN, NodeConstants.HOME);
+                                               HomeRepository homeRepository = new HomeRepository(repository, true);
+                                               bc.registerService(Repository.class, homeRepository, homeProps);
+                                       }
+                               } catch (Exception e) {
+                                       // TODO Auto-generated catch block
+                                       e.printStackTrace();
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new CmsException("Cannot create Jackrabbit repository " + pid, e);
+               }
+
+       }
+
+       @Override
+       public void deleted(String pid) {
+               RepositoryContext repositoryContext = repositories.remove(pid);
+               repositoryContext.getRepository().shutdown();
+               if (log.isDebugEnabled())
+                       log.debug("Deleted repository " + pid);
+       }
+
+       public void shutdown() {
+               for (String pid : repositories.keySet()) {
+                       try {
+                               repositories.get(pid).getRepository().shutdown();
+                               if (log.isDebugEnabled())
+                                       log.debug("Shut down repository " + pid
+                                                       + (pidToCn.containsKey(pid) ? " (" + pidToCn.get(pid) + ")" : ""));
+                       } catch (Exception e) {
+                               log.error("Error when shutting down Jackrabbit repository " + pid, e);
+                       }
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/SecurityProfile.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/SecurityProfile.java
new file mode 100644 (file)
index 0000000..358b212
--- /dev/null
@@ -0,0 +1,288 @@
+package org.argeo.cms.internal.kernel;
+
+import java.io.FilePermission;
+import java.lang.reflect.ReflectPermission;
+import java.net.SocketPermission;
+import java.security.AllPermission;
+import java.util.PropertyPermission;
+
+import javax.management.MBeanPermission;
+import javax.management.MBeanServerPermission;
+import javax.management.MBeanTrustPermission;
+import javax.security.auth.AuthPermission;
+
+import org.osgi.framework.AdminPermission;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServicePermission;
+import org.osgi.service.cm.ConfigurationPermission;
+import org.osgi.service.condpermadmin.BundleLocationCondition;
+import org.osgi.service.condpermadmin.ConditionInfo;
+import org.osgi.service.condpermadmin.ConditionalPermissionAdmin;
+import org.osgi.service.condpermadmin.ConditionalPermissionInfo;
+import org.osgi.service.condpermadmin.ConditionalPermissionUpdate;
+import org.osgi.service.permissionadmin.PermissionInfo;
+
+import bitronix.tm.BitronixTransactionManager;
+
+public interface SecurityProfile {
+       BundleContext bc = FrameworkUtil.getBundle(SecurityProfile.class).getBundleContext();
+
+       default void applySystemPermissions(ConditionalPermissionAdmin permissionAdmin) {
+               ConditionalPermissionUpdate update = permissionAdmin.newConditionalPermissionUpdate();
+               // Self
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { locate(SecurityProfile.class) }) },
+                                               new PermissionInfo[] { new PermissionInfo(AllPermission.class.getName(), null, null) },
+                                               ConditionalPermissionInfo.ALLOW));
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { bc.getBundle(0).getLocation() }) },
+                                               new PermissionInfo[] { new PermissionInfo(AllPermission.class.getName(), null, null) },
+                                               ConditionalPermissionInfo.ALLOW));
+               // All
+               // FIXME understand why Jetty and Jackrabbit require that
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null, null, new PermissionInfo[] {
+                                               new PermissionInfo(SocketPermission.class.getName(), "localhost:7070", "listen,resolve"),
+                                               new PermissionInfo(FilePermission.class.getName(), "<<ALL FILES>>", "read,write,delete"),
+                                               new PermissionInfo(PropertyPermission.class.getName(), "DEBUG", "read"),
+                                               new PermissionInfo(PropertyPermission.class.getName(), "STOP.*", "read"),
+                                               new PermissionInfo(PropertyPermission.class.getName(), "org.apache.jackrabbit.*", "read"),
+                                               new PermissionInfo(RuntimePermission.class.getName(), "*", "*"), },
+                                               ConditionalPermissionInfo.ALLOW));
+
+               // Eclipse
+               // update.getConditionalPermissionInfos()
+               // .add(permissionAdmin.newConditionalPermissionInfo(null,
+               // new ConditionInfo[] { new
+               // ConditionInfo(BundleLocationCondition.class.getName(),
+               // new String[] { "*/org.eclipse.*" }) },
+               // new PermissionInfo[] { new
+               // PermissionInfo(RuntimePermission.class.getName(), "*", "*"),
+               // new PermissionInfo(AdminPermission.class.getName(), "*", "*"),
+               // new PermissionInfo(ServicePermission.class.getName(), "*", "get"),
+               // new PermissionInfo(ServicePermission.class.getName(), "*",
+               // "register"),
+               // new PermissionInfo(TopicPermission.class.getName(), "*", "publish"),
+               // new PermissionInfo(TopicPermission.class.getName(), "*",
+               // "subscribe"),
+               // new PermissionInfo(PropertyPermission.class.getName(), "osgi.*",
+               // "read"),
+               // new PermissionInfo(PropertyPermission.class.getName(), "eclipse.*",
+               // "read"),
+               // new PermissionInfo(PropertyPermission.class.getName(),
+               // "org.eclipse.*", "read"),
+               // new PermissionInfo(PropertyPermission.class.getName(), "equinox.*",
+               // "read"),
+               // new PermissionInfo(PropertyPermission.class.getName(), "xml.*",
+               // "read"),
+               // new PermissionInfo("org.eclipse.equinox.log.LogPermission", "*",
+               // "log"), },
+               // ConditionalPermissionInfo.ALLOW));
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { "*/org.eclipse.*" }) },
+                                               new PermissionInfo[] { new PermissionInfo(AllPermission.class.getName(), null, null), },
+                                               ConditionalPermissionInfo.ALLOW));
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { "*/org.apache.felix.*" }) },
+                                               new PermissionInfo[] { new PermissionInfo(AllPermission.class.getName(), null, null), },
+                                               ConditionalPermissionInfo.ALLOW));
+
+               // Configuration admin
+//             update.getConditionalPermissionInfos().add(permissionAdmin.newConditionalPermissionInfo(null,
+//                             new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+//                                             new String[] { locate(configurationAdmin.getService().getClass()) }) },
+//                             new PermissionInfo[] { new PermissionInfo(ConfigurationPermission.class.getName(), "*", "configure"),
+//                                             new PermissionInfo(AdminPermission.class.getName(), "*", "*"),
+//                                             new PermissionInfo(PropertyPermission.class.getName(), "osgi.*", "read"), },
+//                             ConditionalPermissionInfo.ALLOW));
+
+               // Bitronix
+               update.getConditionalPermissionInfos().add(permissionAdmin.newConditionalPermissionInfo(null,
+                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                               new String[] { locate(BitronixTransactionManager.class) }) },
+                               new PermissionInfo[] { new PermissionInfo(PropertyPermission.class.getName(), "bitronix.tm.*", "read"),
+                                               new PermissionInfo(RuntimePermission.class.getName(), "getClassLoader", null),
+                                               new PermissionInfo(MBeanServerPermission.class.getName(), "createMBeanServer", null),
+                                               new PermissionInfo(MBeanPermission.class.getName(), "bitronix.tm.*", "registerMBean"),
+                                               new PermissionInfo(MBeanTrustPermission.class.getName(), "register", null) },
+                               ConditionalPermissionInfo.ALLOW));
+
+               // DS
+               Bundle dsBundle = findBundle("org.eclipse.equinox.ds");
+               update.getConditionalPermissionInfos().add(permissionAdmin.newConditionalPermissionInfo(null,
+                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                               new String[] { dsBundle.getLocation() }) },
+                               new PermissionInfo[] { new PermissionInfo(ConfigurationPermission.class.getName(), "*", "configure"),
+                                               new PermissionInfo(AdminPermission.class.getName(), "*", "*"),
+                                               new PermissionInfo(ServicePermission.class.getName(), "*", "get"),
+                                               new PermissionInfo(ServicePermission.class.getName(), "*", "register"),
+                                               new PermissionInfo(PropertyPermission.class.getName(), "osgi.*", "read"),
+                                               new PermissionInfo(PropertyPermission.class.getName(), "xml.*", "read"),
+                                               new PermissionInfo(PropertyPermission.class.getName(), "equinox.*", "read"),
+                                               new PermissionInfo(RuntimePermission.class.getName(), "accessDeclaredMembers", null),
+                                               new PermissionInfo(RuntimePermission.class.getName(), "getClassLoader", null),
+                                               new PermissionInfo(ReflectPermission.class.getName(), "suppressAccessChecks", null), },
+                               ConditionalPermissionInfo.ALLOW));
+
+               // Jetty
+               Bundle jettyUtilBundle = findBundle("org.eclipse.equinox.http.jetty");
+               update.getConditionalPermissionInfos().add(permissionAdmin.newConditionalPermissionInfo(null,
+                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                               new String[] { "*/org.eclipse.jetty.*" }) },
+                               new PermissionInfo[] {
+                                               new PermissionInfo(FilePermission.class.getName(), "<<ALL FILES>>", "read,write,delete"), },
+                               ConditionalPermissionInfo.ALLOW));
+
+               // Blueprint
+               Bundle blueprintBundle = findBundle("org.eclipse.gemini.blueprint.core");
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { blueprintBundle.getLocation() }) },
+                                               new PermissionInfo[] { new PermissionInfo(RuntimePermission.class.getName(), "*", null),
+                                                               new PermissionInfo(AdminPermission.class.getName(), "*", "*"), },
+                                               ConditionalPermissionInfo.ALLOW));
+               Bundle blueprintExtenderBundle = findBundle("org.eclipse.gemini.blueprint.extender");
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin
+                                               .newConditionalPermissionInfo(null,
+                                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                                               new String[] { blueprintExtenderBundle.getLocation() }) },
+                                                               new PermissionInfo[] { new PermissionInfo(RuntimePermission.class.getName(), "*", null),
+                                                                               new PermissionInfo(PropertyPermission.class.getName(), "org.eclipse.gemini.*",
+                                                                                               "read"),
+                                                                               new PermissionInfo(AdminPermission.class.getName(), "*", "*"),
+                                                                               new PermissionInfo(ServicePermission.class.getName(), "*", "register"), },
+                                                               ConditionalPermissionInfo.ALLOW));
+               Bundle springCoreBundle = findBundle("org.springframework.core");
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { springCoreBundle.getLocation() }) },
+                                               new PermissionInfo[] { new PermissionInfo(RuntimePermission.class.getName(), "*", null),
+                                                               new PermissionInfo(AdminPermission.class.getName(), "*", "*"), },
+                                               ConditionalPermissionInfo.ALLOW));
+               Bundle blueprintIoBundle = findBundle("org.eclipse.gemini.blueprint.io");
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { blueprintIoBundle.getLocation() }) },
+                                               new PermissionInfo[] { new PermissionInfo(RuntimePermission.class.getName(), "*", null),
+                                                               new PermissionInfo(AdminPermission.class.getName(), "*", "*"), },
+                                               ConditionalPermissionInfo.ALLOW));
+
+               // Equinox
+               Bundle registryBundle = findBundle("org.eclipse.equinox.registry");
+               update.getConditionalPermissionInfos().add(permissionAdmin.newConditionalPermissionInfo(null,
+                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                               new String[] { registryBundle.getLocation() }) },
+                               new PermissionInfo[] { new PermissionInfo(PropertyPermission.class.getName(), "eclipse.*", "read"),
+                                               new PermissionInfo(PropertyPermission.class.getName(), "osgi.*", "read"),
+                                               new PermissionInfo(FilePermission.class.getName(), "<<ALL FILES>>", "read,write,delete"), },
+                               ConditionalPermissionInfo.ALLOW));
+
+               Bundle equinoxUtilBundle = findBundle("org.eclipse.equinox.util");
+               update.getConditionalPermissionInfos().add(permissionAdmin.newConditionalPermissionInfo(null,
+                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                               new String[] { equinoxUtilBundle.getLocation() }) },
+                               new PermissionInfo[] { new PermissionInfo(PropertyPermission.class.getName(), "equinox.*", "read"),
+                                               new PermissionInfo(ServicePermission.class.getName(), "*", "get"),
+                                               new PermissionInfo(ServicePermission.class.getName(), "*", "register"), },
+                               ConditionalPermissionInfo.ALLOW));
+               Bundle equinoxCommonBundle = findBundle("org.eclipse.equinox.common");
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { equinoxCommonBundle.getLocation() }) },
+                                               new PermissionInfo[] { new PermissionInfo(AdminPermission.class.getName(), "*", "*"), },
+                                               ConditionalPermissionInfo.ALLOW));
+
+               Bundle consoleBundle = findBundle("org.eclipse.equinox.console");
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { consoleBundle.getLocation() }) },
+                                               new PermissionInfo[] { new PermissionInfo(ServicePermission.class.getName(), "*", "register"),
+                                                               new PermissionInfo(AdminPermission.class.getName(), "*", "listener") },
+                                               ConditionalPermissionInfo.ALLOW));
+               Bundle preferencesBundle = findBundle("org.eclipse.equinox.preferences");
+               update.getConditionalPermissionInfos().add(permissionAdmin.newConditionalPermissionInfo(null,
+                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                               new String[] { preferencesBundle.getLocation() }) },
+                               new PermissionInfo[] {
+                                               new PermissionInfo(FilePermission.class.getName(), "<<ALL FILES>>", "read,write,delete"), },
+                               ConditionalPermissionInfo.ALLOW));
+               Bundle appBundle = findBundle("org.eclipse.equinox.app");
+               update.getConditionalPermissionInfos().add(permissionAdmin.newConditionalPermissionInfo(null,
+                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                               new String[] { appBundle.getLocation() }) },
+                               new PermissionInfo[] {
+                                               new PermissionInfo(FilePermission.class.getName(), "<<ALL FILES>>", "read,write,delete"), },
+                               ConditionalPermissionInfo.ALLOW));
+
+               // Jackrabbit
+               Bundle jackrabbitCoreBundle = findBundle("org.apache.jackrabbit.core");
+               update.getConditionalPermissionInfos().add(permissionAdmin.newConditionalPermissionInfo(null,
+                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                               new String[] { jackrabbitCoreBundle.getLocation() }) },
+                               new PermissionInfo[] {
+                                               new PermissionInfo(FilePermission.class.getName(), "<<ALL FILES>>", "read,write,delete"),
+                                               new PermissionInfo(PropertyPermission.class.getName(), "*", "read,write"),
+                                               new PermissionInfo(AuthPermission.class.getName(), "getLoginConfiguration", null),
+                                               new PermissionInfo(AuthPermission.class.getName(), "createLoginContext.Jackrabbit", null), },
+                               ConditionalPermissionInfo.ALLOW));
+               Bundle jackrabbitCommonBundle = findBundle("org.apache.jackrabbit.jcr.commons");
+               update.getConditionalPermissionInfos().add(permissionAdmin.newConditionalPermissionInfo(null,
+                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                               new String[] { jackrabbitCommonBundle.getLocation() }) },
+                               new PermissionInfo[] {
+                                               new PermissionInfo(AuthPermission.class.getName(), "createLoginContext.Jackrabbit", null), },
+                               ConditionalPermissionInfo.ALLOW));
+               Bundle tikaCoreBundle = findBundle("org.apache.tika.core");
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { tikaCoreBundle.getLocation() }) },
+                                               new PermissionInfo[] { new PermissionInfo(PropertyPermission.class.getName(), "*", "read"),
+                                                               new PermissionInfo(AdminPermission.class.getName(), "*", "*") },
+                                               ConditionalPermissionInfo.ALLOW));
+               Bundle luceneBundle = findBundle("org.apache.lucene");
+               update.getConditionalPermissionInfos()
+                               .add(permissionAdmin.newConditionalPermissionInfo(null,
+                                               new ConditionInfo[] { new ConditionInfo(BundleLocationCondition.class.getName(),
+                                                               new String[] { luceneBundle.getLocation() }) },
+                                               new PermissionInfo[] {
+                                                               new PermissionInfo(FilePermission.class.getName(), "<<ALL FILES>>",
+                                                                               "read,write,delete"),
+                                                               new PermissionInfo(PropertyPermission.class.getName(), "*", "read"),
+                                                               new PermissionInfo(AdminPermission.class.getName(), "*", "*") },
+                                               ConditionalPermissionInfo.ALLOW));
+
+               // COMMIT
+               update.commit();
+       }
+
+       /** @return bundle location */
+       default String locate(Class<?> clzz) {
+               return FrameworkUtil.getBundle(clzz).getLocation();
+       }
+
+       /** Can be null */
+       default Bundle findBundle(String symbolicName) {
+               for (Bundle b : bc.getBundles())
+                       if (b.getSymbolicName().equals(symbolicName))
+                               return b;
+               return null;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/dc=example,dc=com.ldif b/org.argeo.cms/src/org/argeo/cms/internal/kernel/dc=example,dc=com.ldif
new file mode 100644 (file)
index 0000000..43e7ade
--- /dev/null
@@ -0,0 +1,41 @@
+dn: dc=example,dc=com
+objectClass: domain
+objectClass: extensibleObject
+objectClass: top
+dc: example
+
+dn: ou=Groups,dc=example,dc=com
+objectClass: organizationalUnit
+objectClass: top
+ou: Groups
+
+dn: ou=People,dc=example,dc=com
+objectClass: organizationalUnit
+objectClass: top
+ou: People
+
+dn: uid=demo,ou=People,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Demo User
+description: Demo user
+givenName: Demo
+mail: demo@localhost
+sn: User
+uid: demo
+userPassword:: e1NIQX1pZVNWNTVRYytlUU9hWURSU2hhL0Fqek5USkU9
+
+dn: uid=root,ou=People,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: person
+objectClass: organizationalPerson
+objectClass: top
+cn: Super User
+description: Superuser
+givenName: Super
+mail: root@localhost
+sn: User
+uid: root
+userPassword:: e1NIQX1pZVNWNTVRYytlUU9hWURSU2hhL0Fqek5USkU9
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas-ipa.cfg b/org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas-ipa.cfg
new file mode 100644 (file)
index 0000000..018c1bf
--- /dev/null
@@ -0,0 +1,40 @@
+USER {
+    org.argeo.cms.auth.HttpSessionLoginModule sufficient;
+    org.argeo.cms.auth.SpnegoLoginModule optional;
+    com.sun.security.auth.module.Krb5LoginModule optional tryFirstPass=true;
+    org.argeo.cms.auth.UserAdminLoginModule sufficient;
+};
+
+ANONYMOUS {
+    org.argeo.cms.auth.HttpSessionLoginModule sufficient;
+    org.argeo.cms.auth.AnonymousLoginModule sufficient;
+};
+
+DATA_ADMIN {
+    org.argeo.cms.auth.DataAdminLoginModule requisite;
+};
+
+NODE {
+    com.sun.security.auth.module.Krb5LoginModule optional
+     keyTab="${osgi.instance.area}node/krb5.keytab" 
+     useKeyTab=true
+     storeKey=true;
+    org.argeo.cms.auth.DataAdminLoginModule requisite;
+};
+
+KEYRING {
+    org.argeo.cms.auth.KeyringLoginModule required;
+};
+
+SINGLE_USER {
+    com.sun.security.auth.module.Krb5LoginModule optional
+     principal="${user.name}"
+     storeKey=true
+     useTicketCache=true
+     debug=true;
+    org.argeo.cms.auth.SingleUserLoginModule requisite;
+};
+
+Jackrabbit {
+   org.argeo.security.jackrabbit.SystemJackrabbitLoginModule requisite;
+};
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas.cfg b/org.argeo.cms/src/org/argeo/cms/internal/kernel/jaas.cfg
new file mode 100644 (file)
index 0000000..e32c23f
--- /dev/null
@@ -0,0 +1,29 @@
+USER {
+    org.argeo.cms.auth.HttpSessionLoginModule sufficient;
+    org.argeo.cms.auth.UserAdminLoginModule sufficient;
+};
+
+ANONYMOUS {
+    org.argeo.cms.auth.HttpSessionLoginModule sufficient;
+    org.argeo.cms.auth.AnonymousLoginModule sufficient;
+};
+
+DATA_ADMIN {
+    org.argeo.cms.auth.DataAdminLoginModule requisite;
+};
+
+NODE {
+    org.argeo.cms.auth.DataAdminLoginModule requisite;
+};
+
+KEYRING {
+    org.argeo.cms.auth.KeyringLoginModule required;
+};
+
+SINGLE_USER {
+    org.argeo.cms.auth.SingleUserLoginModule requisite;
+};
+
+Jackrabbit {
+   org.argeo.security.jackrabbit.SystemJackrabbitLoginModule requisite;
+};
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/ou=roles,ou=node.ldif b/org.argeo.cms/src/org/argeo/cms/internal/kernel/ou=roles,ou=node.ldif
new file mode 100644 (file)
index 0000000..85247ed
--- /dev/null
@@ -0,0 +1,9 @@
+dn: ou=node
+objectClass: organizationalUnit
+objectClass: top
+ou: node
+
+dn: ou=roles,ou=node
+objectClass: organizationalUnit
+objectClass: top
+ou: roles
diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/ou=tokens,ou=node.ldif b/org.argeo.cms/src/org/argeo/cms/internal/kernel/ou=tokens,ou=node.ldif
new file mode 100644 (file)
index 0000000..4ae9b88
--- /dev/null
@@ -0,0 +1,4 @@
+dn: ou=tokens,ou=node
+objectClass: organizationalUnit
+objectClass: top
+ou: tokens
diff --git a/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java b/org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java
new file mode 100644 (file)
index 0000000..779406a
--- /dev/null
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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.AccessController;
+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.apache.commons.io.IOUtils;
+import org.argeo.cms.CmsException;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.security.CryptoKeyring;
+import org.argeo.node.security.Keyring;
+import org.argeo.node.security.PBEKeySpecCallback;
+
+/** 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 = Subject.getSubject(AccessController.getContext());
+               // 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(NodeConstants.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 CmsException("Keyring login failed", e);
+                       } finally {
+                               Thread.currentThread().setContextClassLoader(currentContextClassLoader);
+                       }
+
+               } else {
+                       SecretKey secretKey = iterator.next();
+                       if (iterator.hasNext())
+                               throw new CmsException("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);) {
+                       IOUtils.copy(reader, writer);
+                       return writer.toCharArray();
+               } catch (IOException e) {
+                       throw new CmsException("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 CmsException("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 CmsException("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 CmsException("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
new file mode 100644 (file)
index 0000000..c5b4e9e
--- /dev/null
@@ -0,0 +1,157 @@
+package org.argeo.cms.security;
+
+import static javax.xml.bind.DatatypeConverter.printBase64Binary;
+
+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.util.zip.Checksum;
+
+import org.argeo.cms.CmsException;
+
+/** 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 (Exception e) {
+                       throw new CmsException("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 (Exception e) {
+                       throw new CmsException("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);
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/security/JcrKeyring.java b/org.argeo.cms/src/org/argeo/cms/security/JcrKeyring.java
new file mode 100644 (file)
index 0000000..9aeb760
--- /dev/null
@@ -0,0 +1,411 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.security;
+
+import java.io.ByteArrayInputStream;
+import java.io.CharArrayReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.Provider;
+import java.security.SecureRandom;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.query.Query;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.ArgeoNames;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.cms.CmsException;
+import org.argeo.jcr.ArgeoJcrException;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.NodeUtils;
+import org.argeo.node.security.PBEKeySpecCallback;
+
+/** JCR based implementation of a keyring */
+public class JcrKeyring extends AbstractKeyring implements ArgeoNames {
+       private final static Log log = LogFactory.getLog(JcrKeyring.class);
+       /**
+        * Stronger with 256, but causes problem with Oracle JVM, force 128 in this case
+        */
+       public final static Long DEFAULT_SECRETE_KEY_LENGTH = 256l;
+       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";
+
+       private Integer iterationCountFactor = 200;
+       private Long secretKeyLength = DEFAULT_SECRETE_KEY_LENGTH;
+       private String secretKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY;
+       private String secretKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION;
+       private String cipherName = DEFAULT_CIPHER_NAME;
+
+       private final Repository repository;
+       // TODO remove thread local session ; open a session each time
+       private ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<Session>() {
+
+               @Override
+               protected Session initialValue() {
+                       return login();
+               }
+
+       };
+
+       // FIXME is it really still needed?
+       /**
+        * When setup is called the session has not yet been saved and we don't want to
+        * save it since there maybe other data which would be inconsistent. So we keep
+        * a reference to this node which will then be used (an reset to null) when
+        * handling the PBE callback. We keep one per thread in case multiple users are
+        * accessing the same instance of a keyring.
+        */
+       // private ThreadLocal<Node> notYetSavedKeyring = new ThreadLocal<Node>() {
+       //
+       // @Override
+       // protected Node initialValue() {
+       // return null;
+       // }
+       // };
+
+       public JcrKeyring(Repository repository) {
+               this.repository = repository;
+       }
+
+       private Session session() {
+               Session session = this.sessionThreadLocal.get();
+               if (!session.isLive()) {
+                       session = login();
+                       sessionThreadLocal.set(session);
+               }
+               return session;
+       }
+
+       private Session login() {
+               try {
+                       return repository.login();
+               } catch (RepositoryException e) {
+                       throw new CmsException("Cannot login key ring session", e);
+               }
+       }
+
+       @Override
+       protected synchronized Boolean isSetup() {
+               Session session = null;
+               try {
+                       // if (notYetSavedKeyring.get() != null)
+                       // return true;
+                       session = session();
+                       session.refresh(true);
+                       Node userHome = NodeUtils.getUserHome(session);
+                       return userHome.hasNode(ARGEO_KEYRING);
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot check whether keyring is setup", e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       @Override
+       protected synchronized void setup(char[] password) {
+               Binary binary = null;
+               // InputStream in = null;
+               try {
+                       session().refresh(true);
+                       Node userHome = NodeUtils.getUserHome(session());
+                       Node keyring;
+                       if (userHome.hasNode(ARGEO_KEYRING)) {
+                               throw new CmsException("Keyring already set up");
+                       } else {
+                               keyring = userHome.addNode(ARGEO_KEYRING);
+                       }
+                       keyring.addMixin(ArgeoTypes.ARGEO_PBE_SPEC);
+
+                       // deterministic salt and iteration count based on username
+                       String username = session().getUserID();
+                       byte[] salt = new byte[8];
+                       byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);
+                       for (int i = 0; i < salt.length; i++) {
+                               if (i < usernameBytes.length)
+                                       salt[i] = usernameBytes[i];
+                               else
+                                       salt[i] = 0;
+                       }
+                       try (InputStream in = new ByteArrayInputStream(salt);) {
+                               binary = session().getValueFactory().createBinary(in);
+                       }
+                       keyring.setProperty(ARGEO_SALT, binary);
+
+                       Integer iterationCount = username.length() * iterationCountFactor;
+                       keyring.setProperty(ARGEO_ITERATION_COUNT, iterationCount);
+
+                       // default algo
+                       // TODO check if algo and key length are available, use DES if not
+                       keyring.setProperty(ARGEO_SECRET_KEY_FACTORY, secretKeyFactoryName);
+                       keyring.setProperty(ARGEO_KEY_LENGTH, secretKeyLength);
+                       keyring.setProperty(ARGEO_SECRET_KEY_ENCRYPTION, secretKeyEncryption);
+                       keyring.setProperty(ARGEO_CIPHER, cipherName);
+
+                       keyring.getSession().save();
+
+                       // encrypted password hash
+                       // IOUtils.closeQuietly(in);
+                       // JcrUtils.closeQuietly(binary);
+                       // byte[] btPass = hash(password, salt, iterationCount);
+                       // in = new ByteArrayInputStream(btPass);
+                       // binary = session().getValueFactory().createBinary(in);
+                       // keyring.setProperty(ARGEO_PASSWORD, binary);
+
+                       // notYetSavedKeyring.set(keyring);
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot setup keyring", e);
+               } finally {
+                       JcrUtils.closeQuietly(binary);
+                       // IOUtils.closeQuietly(in);
+                       // JcrUtils.discardQuietly(session());
+               }
+       }
+
+       @Override
+       protected synchronized void handleKeySpecCallback(PBEKeySpecCallback pbeCallback) {
+               Session session = null;
+               try {
+                       session = session();
+                       session.refresh(true);
+                       Node userHome = NodeUtils.getUserHome(session);
+                       Node keyring;
+                       if (userHome.hasNode(ARGEO_KEYRING))
+                               keyring = userHome.getNode(ARGEO_KEYRING);
+                       // else if (notYetSavedKeyring.get() != null)
+                       // keyring = notYetSavedKeyring.get();
+                       else
+                               throw new ArgeoJcrException("Keyring not setup");
+
+                       pbeCallback.set(keyring.getProperty(ARGEO_SECRET_KEY_FACTORY).getString(),
+                                       JcrUtils.getBinaryAsBytes(keyring.getProperty(ARGEO_SALT)),
+                                       (int) keyring.getProperty(ARGEO_ITERATION_COUNT).getLong(),
+                                       (int) keyring.getProperty(ARGEO_KEY_LENGTH).getLong(),
+                                       keyring.getProperty(ARGEO_SECRET_KEY_ENCRYPTION).getString());
+
+                       // if (notYetSavedKeyring.get() != null)
+                       // notYetSavedKeyring.remove();
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot handle key spec callback", e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       /** The parent node must already exist at this path. */
+       @Override
+       protected synchronized void encrypt(String path, InputStream unencrypted) {
+               // should be called first for lazy initialization
+               SecretKey secretKey = getSecretKey(null);
+               Cipher cipher = createCipher();
+
+               // Binary binary = null;
+               // InputStream in = null;
+               try {
+                       session().refresh(true);
+                       Node node;
+                       if (!session().nodeExists(path)) {
+                               String parentPath = JcrUtils.parentPath(path);
+                               if (!session().nodeExists(parentPath))
+                                       throw new ArgeoJcrException("No parent node of " + path);
+                               Node parentNode = session().getNode(parentPath);
+                               node = parentNode.addNode(JcrUtils.nodeNameFromPath(path));
+                       } else {
+                               node = session().getNode(path);
+                       }
+                       encrypt(secretKey, cipher, node, unencrypted);
+                       // node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED);
+                       // SecureRandom random = new SecureRandom();
+                       // byte[] iv = new byte[16];
+                       // random.nextBytes(iv);
+                       // cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+                       // JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv);
+                       //
+                       // try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
+                       // binary = session().getValueFactory().createBinary(in);
+                       // node.setProperty(Property.JCR_DATA, binary);
+                       // session().save();
+                       // }
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot encrypt", e);
+               } finally {
+                       try {
+                               unencrypted.close();
+                       } catch (IOException e) {
+                               // silent
+                       }
+                       // IOUtils.closeQuietly(unencrypted);
+                       // IOUtils.closeQuietly(in);
+                       // JcrUtils.closeQuietly(binary);
+                       JcrUtils.logoutQuietly(session());
+               }
+       }
+
+       protected synchronized void encrypt(SecretKey secretKey, Cipher cipher, Node node, InputStream unencrypted) {
+               try {
+                       node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED);
+                       SecureRandom random = new SecureRandom();
+                       byte[] iv = new byte[16];
+                       random.nextBytes(iv);
+                       cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
+                       JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv);
+
+                       Binary binary = null;
+                       try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
+                               binary = session().getValueFactory().createBinary(in);
+                               node.setProperty(Property.JCR_DATA, binary);
+                               session().save();
+                       } finally {
+                               JcrUtils.closeQuietly(binary);
+                       }
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot encrypt", e);
+               } finally {
+                       try {
+                               unencrypted.close();
+                       } catch (IOException e) {
+                               // silent
+                       }
+                       // IOUtils.closeQuietly(unencrypted);
+                       // IOUtils.closeQuietly(in);
+                       // JcrUtils.closeQuietly(binary);
+                       // JcrUtils.logoutQuietly(session());
+               }
+       }
+
+       @Override
+       protected synchronized InputStream decrypt(String path) {
+               Binary binary = null;
+               // InputStream encrypted = null;
+               try {
+                       session().refresh(true);
+                       if (!session().nodeExists(path)) {
+                               char[] password = ask();
+                               Reader reader = new CharArrayReader(password);
+                               return new ByteArrayInputStream(IOUtils.toByteArray(reader, StandardCharsets.UTF_8));
+                       } else {
+                               // should be called first for lazy initialisation
+                               SecretKey secretKey = getSecretKey(null);
+                               Cipher cipher = createCipher();
+                               Node node = session().getNode(path);
+                               return decrypt(secretKey, cipher, node);
+                       }
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot decrypt", e);
+               } finally {
+                       // IOUtils.closeQuietly(encrypted);
+                       // IOUtils.closeQuietly(reader);
+                       JcrUtils.closeQuietly(binary);
+                       JcrUtils.logoutQuietly(session());
+               }
+       }
+
+       protected synchronized InputStream decrypt(SecretKey secretKey, Cipher cipher, Node node)
+                       throws RepositoryException, GeneralSecurityException {
+               if (node.hasProperty(ARGEO_IV)) {
+                       byte[] iv = JcrUtils.getBinaryAsBytes(node.getProperty(ARGEO_IV));
+                       cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
+               } else {
+                       cipher.init(Cipher.DECRYPT_MODE, secretKey);
+               }
+
+               Binary binary = node.getProperty(Property.JCR_DATA).getBinary();
+               InputStream encrypted = binary.getStream();
+               return new CipherInputStream(encrypted, cipher);
+       }
+
+       protected Cipher createCipher() {
+               try {
+                       Node userHome = NodeUtils.getUserHome(session());
+                       if (!userHome.hasNode(ARGEO_KEYRING))
+                               throw new ArgeoJcrException("Keyring not setup");
+                       Node keyring = userHome.getNode(ARGEO_KEYRING);
+                       String cipherName = keyring.getProperty(ARGEO_CIPHER).getString();
+                       Provider securityProvider = getSecurityProvider();
+                       Cipher cipher;
+                       if (securityProvider == null)// TODO use BC?
+                               cipher = Cipher.getInstance(cipherName);
+                       else
+                               cipher = Cipher.getInstance(cipherName, securityProvider);
+                       return cipher;
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot get cipher", e);
+               }
+       }
+
+       public synchronized void changePassword(char[] oldPassword, char[] newPassword) {
+               // TODO make it XA compatible
+               SecretKey oldSecretKey = getSecretKey(oldPassword);
+               SecretKey newSecretKey = getSecretKey(newPassword);
+               Session session = session();
+               try {
+                       NodeIterator encryptedNodes = session.getWorkspace().getQueryManager()
+                                       .createQuery("select * from [argeo:encrypted]", Query.JCR_SQL2).execute().getNodes();
+                       while (encryptedNodes.hasNext()) {
+                               Node node = encryptedNodes.nextNode();
+                               InputStream in = decrypt(oldSecretKey, createCipher(), node);
+                               encrypt(newSecretKey, createCipher(), node, in);
+                               if (log.isDebugEnabled())
+                                       log.debug("Converted keyring encrypted value of " + node.getPath());
+                       }
+               } catch (RepositoryException | GeneralSecurityException e) {
+                       throw new CmsException("Cannot change JCR keyring password", e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       // public synchronized void setSession(Session session) {
+       // this.session = session;
+       // }
+
+       public void setIterationCountFactor(Integer iterationCountFactor) {
+               this.iterationCountFactor = iterationCountFactor;
+       }
+
+       public void setSecretKeyLength(Long keyLength) {
+               this.secretKeyLength = keyLength;
+       }
+
+       public void setSecretKeyFactoryName(String secreteKeyFactoryName) {
+               this.secretKeyFactoryName = secreteKeyFactoryName;
+       }
+
+       public void setSecretKeyEncryption(String secreteKeyEncryption) {
+               this.secretKeyEncryption = secreteKeyEncryption;
+       }
+
+       public void setCipherName(String cipherName) {
+               this.cipherName = cipherName;
+       }
+
+}
\ No newline at end of file
diff --git a/org.argeo.cms/src/org/argeo/cms/spring/AbstractSystemExecution.java b/org.argeo.cms/src/org/argeo/cms/spring/AbstractSystemExecution.java
new file mode 100644 (file)
index 0000000..ce92c46
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.spring;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.argeo.node.NodeConstants;
+
+/** Provides base method for executing code with system authorization. */
+abstract class AbstractSystemExecution {
+       private final static Log log = LogFactory.getLog(AbstractSystemExecution.class);
+       private final Subject subject = new Subject();
+
+       /** Authenticate the calling thread */
+       protected void authenticateAsSystem() {
+               ClassLoader origClassLoader = Thread.currentThread().getContextClassLoader();
+               Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+               try {
+                       LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_DATA_ADMIN, subject);
+                       lc.login();
+               } catch (LoginException e) {
+                       throw new CmsException("Cannot login as system", e);
+               } finally {
+                       Thread.currentThread().setContextClassLoader(origClassLoader);
+               }
+               if (log.isTraceEnabled())
+                       log.trace("System authenticated");
+       }
+
+       protected void deauthenticateAsSystem() {
+               ClassLoader origClassLoader = Thread.currentThread().getContextClassLoader();
+               Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
+               try {
+                       LoginContext lc = new LoginContext(NodeConstants.LOGIN_CONTEXT_DATA_ADMIN, subject);
+                       lc.logout();
+               } catch (LoginException e) {
+                       throw new CmsException("Cannot logout as system", e);
+               } finally {
+                       Thread.currentThread().setContextClassLoader(origClassLoader);
+               }
+       }
+
+       protected Subject getSubject() {
+               return subject;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/spring/AuthenticatedApplicationContextInitialization.java b/org.argeo.cms/src/org/argeo/cms/spring/AuthenticatedApplicationContextInitialization.java
new file mode 100644 (file)
index 0000000..e1af582
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.spring;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.security.auth.Subject;
+
+import org.eclipse.gemini.blueprint.context.DependencyInitializationAwareBeanPostProcessor;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.support.AbstractBeanFactory;
+import org.springframework.beans.factory.support.SecurityContextProvider;
+import org.springframework.beans.factory.support.SimpleSecurityContextProvider;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+
+/**
+ * Executes with a system authentication the instantiation and initialization
+ * methods of the application context where it has been defined.
+ */
+public class AuthenticatedApplicationContextInitialization extends
+               AbstractSystemExecution implements
+               DependencyInitializationAwareBeanPostProcessor, ApplicationContextAware {
+       /** If non empty, restricts to these beans */
+       private List<String> beanNames = new ArrayList<String>();
+
+       public Object postProcessBeforeInitialization(Object bean, String beanName)
+                       throws BeansException {
+               if (beanNames.size() == 0 || beanNames.contains(beanName))
+                       authenticateAsSystem();
+               return bean;
+       }
+
+       public Object postProcessAfterInitialization(Object bean, String beanName)
+                       throws BeansException {
+               if (beanNames.size() == 0 || beanNames.contains(beanName))
+                       deauthenticateAsSystem();
+               return bean;
+       }
+
+       public void setBeanNames(List<String> beanNames) {
+               this.beanNames = beanNames;
+       }
+
+       @Override
+       public void setApplicationContext(ApplicationContext applicationContext)
+                       throws BeansException {
+               if (applicationContext.getAutowireCapableBeanFactory() instanceof AbstractBeanFactory) {
+                       final AbstractBeanFactory beanFactory = ((AbstractBeanFactory) applicationContext
+                                       .getAutowireCapableBeanFactory());
+                       // retrieve subject's access control context
+                       // and set it as the bean factory security context
+                       Subject.doAs(getSubject(), new PrivilegedAction<Void>() {
+                               @Override
+                               public Void run() {
+                                       SecurityContextProvider scp = new SimpleSecurityContextProvider(
+                                                       AccessController.getContext());
+                                       beanFactory.setSecurityContextProvider(scp);
+                                       return null;
+                               }
+                       });
+               }
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/spring/SimpleRoleRegistration.java b/org.argeo.cms/src/org/argeo/cms/spring/SimpleRoleRegistration.java
new file mode 100644 (file)
index 0000000..255ce11
--- /dev/null
@@ -0,0 +1,89 @@
+package org.argeo.cms.spring;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.transaction.UserTransaction;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.cms.CmsException;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdmin;
+
+/**
+ * Register one or many roles via a user admin service. Does nothing if the role
+ * is already registered.
+ */
+public class SimpleRoleRegistration implements Runnable {
+       private final static Log log = LogFactory
+                       .getLog(SimpleRoleRegistration.class);
+
+       private String role;
+       private List<String> roles = new ArrayList<String>();
+       private UserAdmin userAdmin;
+       private UserTransaction userTransaction;
+
+       @Override
+       public void run() {
+               try {
+                       userTransaction.begin();
+                       if (role != null && !roleExists(role))
+                               newRole(toDn(role));
+
+                       for (String r : roles)
+                               if (!roleExists(r))
+                                       newRole(toDn(r));
+                       userTransaction.commit();
+               } catch (Exception e) {
+                       try {
+                               userTransaction.rollback();
+                       } catch (Exception e1) {
+                               log.error("Cannot rollback", e1);
+                       }
+                       throw new CmsException("Cannot add roles", e);
+               }
+       }
+
+       private boolean roleExists(String role) {
+               return userAdmin.getRole(toDn(role).toString()) != null;
+       }
+
+       protected void newRole(LdapName r) {
+               userAdmin.createRole(r.toString(), Role.GROUP);
+               log.info("Added role " + r + " required by application.");
+       }
+
+       public void register(UserAdmin userAdminService, Map<?, ?> properties) {
+               this.userAdmin = userAdminService;
+               run();
+       }
+
+       protected LdapName toDn(String name) {
+               try {
+                       return new LdapName("cn=" + name + ",ou=roles,ou=node");
+               } catch (InvalidNameException e) {
+                       throw new CmsException("Badly formatted role name " + name, e);
+               }
+       }
+
+       public void setRole(String role) {
+               this.role = role;
+       }
+
+       public void setRoles(List<String> roles) {
+               this.roles = roles;
+       }
+
+       public void setUserAdmin(UserAdmin userAdminService) {
+               this.userAdmin = userAdminService;
+       }
+
+       public void setUserTransaction(UserTransaction userTransaction) {
+               this.userTransaction = userTransaction;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/spring/osgi/OsgiModuleLabel.java b/org.argeo.cms/src/org/argeo/cms/spring/osgi/OsgiModuleLabel.java
new file mode 100644 (file)
index 0000000..f085d6a
--- /dev/null
@@ -0,0 +1,41 @@
+package org.argeo.cms.spring.osgi;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+
+/**
+ * Logs the name and version of an OSGi bundle based on its
+ * {@link BundleContext}.
+ */
+public class OsgiModuleLabel {
+       private final static Log log = LogFactory.getLog(OsgiModuleLabel.class);
+
+       private Bundle bundle;
+
+       public OsgiModuleLabel() {
+       }
+
+       /** Sets without logging. */
+       public OsgiModuleLabel(Bundle bundle) {
+               this.bundle = bundle;
+       }
+
+       /**
+        * Retrieved bundle from a bundle context and logs it. Typically to be set
+        * as a Spring bean.
+        */
+       public void setBundleContext(BundleContext bundleContext) {
+               this.bundle = bundleContext.getBundle();
+               log.info(msg());
+       }
+
+       public String msg() {
+               String name = bundle.getHeaders().get(Constants.BUNDLE_NAME).toString();
+               String symbolicName = bundle.getSymbolicName();
+               String version = bundle.getVersion().toString();
+               return name + " v" + version + " (" + symbolicName + ")";
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/CsvTabularWriter.java b/org.argeo.cms/src/org/argeo/cms/tabular/CsvTabularWriter.java
new file mode 100644 (file)
index 0000000..d22f44e
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.tabular;
+
+import java.io.OutputStream;
+
+import org.argeo.node.tabular.TabularWriter;
+import org.argeo.util.CsvWriter;
+
+/** Write tabular content in a stream as CSV. Wraps a {@link CsvWriter}. */
+public class CsvTabularWriter implements TabularWriter {
+       private CsvWriter csvWriter;
+
+       public CsvTabularWriter(OutputStream out) {
+               this.csvWriter = new CsvWriter(out);
+       }
+
+       public void appendRow(Object[] row) {
+               csvWriter.writeLine(row);
+       }
+
+       public void close() {
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/JcrTabularRowIterator.java b/org.argeo.cms/src/org/argeo/cms/tabular/JcrTabularRowIterator.java
new file mode 100644 (file)
index 0000000..04dce92
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.tabular;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.jcr.ArgeoJcrException;
+import org.argeo.node.tabular.ArrayTabularRow;
+import org.argeo.node.tabular.TabularColumn;
+import org.argeo.node.tabular.TabularRow;
+import org.argeo.node.tabular.TabularRowIterator;
+import org.argeo.util.CsvParser;
+
+/** Iterates over the rows of a {@link ArgeoTypes#ARGEO_TABLE} node. */
+public class JcrTabularRowIterator implements TabularRowIterator {
+       private Boolean hasNext = null;
+       private Boolean parsingCompleted = false;
+
+       private Long currentRowNumber = 0l;
+
+       private List<TabularColumn> header = new ArrayList<TabularColumn>();
+
+       /** referenced so that we can close it */
+       private Binary binary;
+       private InputStream in;
+
+       private CsvParser csvParser;
+       private ArrayBlockingQueue<List<String>> textLines;
+
+       public JcrTabularRowIterator(Node tableNode) {
+               try {
+                       for (NodeIterator it = tableNode.getNodes(); it.hasNext();) {
+                               Node node = it.nextNode();
+                               if (node.isNodeType(ArgeoTypes.ARGEO_COLUMN)) {
+                                       Integer type = PropertyType.valueFromName(node.getProperty(
+                                                       Property.JCR_REQUIRED_TYPE).getString());
+                                       TabularColumn tc = new TabularColumn(node.getProperty(
+                                                       Property.JCR_TITLE).getString(), type);
+                                       header.add(tc);
+                               }
+                       }
+                       Node contentNode = tableNode.getNode(Property.JCR_CONTENT);
+                       if (contentNode.isNodeType(ArgeoTypes.ARGEO_CSV)) {
+                               textLines = new ArrayBlockingQueue<List<String>>(1000);
+                               csvParser = new CsvParser() {
+                                       protected void processLine(Integer lineNumber,
+                                                       List<String> header, List<String> tokens) {
+                                               try {
+                                                       textLines.put(tokens);
+                                               } catch (InterruptedException e) {
+                                                       // TODO Auto-generated catch block
+                                                       e.printStackTrace();
+                                               }
+                                               // textLines.add(tokens);
+                                               if (hasNext == null) {
+                                                       hasNext = true;
+                                                       synchronized (JcrTabularRowIterator.this) {
+                                                               JcrTabularRowIterator.this.notifyAll();
+                                                       }
+                                               }
+                                       }
+                               };
+                               csvParser.setNoHeader(true);
+                               binary = contentNode.getProperty(Property.JCR_DATA).getBinary();
+                               in = binary.getStream();
+                               Thread thread = new Thread(contentNode.getPath() + " reader") {
+                                       public void run() {
+                                               try {
+                                                       csvParser.parse(in);
+                                               } finally {
+                                                       parsingCompleted = true;
+                                                       IOUtils.closeQuietly(in);
+                                               }
+                                       }
+                               };
+                               thread.start();
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot read table " + tableNode, e);
+               }
+       }
+
+       public synchronized boolean hasNext() {
+               // we don't know if there is anything available
+               // while (hasNext == null)
+               // try {
+               // wait();
+               // } catch (InterruptedException e) {
+               // // silent
+               // // FIXME better deal with interruption
+               // Thread.currentThread().interrupt();
+               // break;
+               // }
+
+               // buffer not empty
+               if (!textLines.isEmpty())
+                       return true;
+
+               // maybe the parsing is finished but the flag has not been set
+               while (!parsingCompleted && textLines.isEmpty())
+                       try {
+                               wait(100);
+                       } catch (InterruptedException e) {
+                               // silent
+                               // FIXME better deal with interruption
+                               Thread.currentThread().interrupt();
+                               break;
+                       }
+
+               // buffer not empty
+               if (!textLines.isEmpty())
+                       return true;
+
+               // (parsingCompleted && textLines.isEmpty())
+               return false;
+
+               // if (!hasNext && textLines.isEmpty()) {
+               // if (in != null) {
+               // IOUtils.closeQuietly(in);
+               // in = null;
+               // }
+               // if (binary != null) {
+               // JcrUtils.closeQuietly(binary);
+               // binary = null;
+               // }
+               // return false;
+               // } else
+               // return true;
+       }
+
+       public synchronized TabularRow next() {
+               try {
+                       List<String> tokens = textLines.take();
+                       List<Object> objs = new ArrayList<Object>(tokens.size());
+                       for (String token : tokens) {
+                               // TODO convert to other formats using header
+                               objs.add(token);
+                       }
+                       currentRowNumber++;
+                       return new ArrayTabularRow(objs);
+               } catch (InterruptedException e) {
+                       // silent
+                       // FIXME better deal with interruption
+               }
+               return null;
+       }
+
+       public void remove() {
+               throw new UnsupportedOperationException();
+       }
+
+       public Long getCurrentRowNumber() {
+               return currentRowNumber;
+       }
+
+       public List<TabularColumn> getHeader() {
+               return header;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/tabular/JcrTabularWriter.java b/org.argeo.cms/src/org/argeo/cms/tabular/JcrTabularWriter.java
new file mode 100644 (file)
index 0000000..a6e1e28
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.cms.tabular;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.List;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.cms.ArgeoTypes;
+import org.argeo.jcr.ArgeoJcrException;
+import org.argeo.jcr.JcrUtils;
+import org.argeo.node.tabular.TabularColumn;
+import org.argeo.node.tabular.TabularWriter;
+import org.argeo.util.CsvWriter;
+
+/** Write / reference tabular content in a JCR repository. */
+public class JcrTabularWriter implements TabularWriter {
+       private Node contentNode;
+       private ByteArrayOutputStream out;
+       private CsvWriter csvWriter;
+       
+       @SuppressWarnings("unused")
+       private final List<TabularColumn> columns;
+
+       /** Creates a table node */
+       public JcrTabularWriter(Node tableNode, List<TabularColumn> columns,
+                       String contentNodeType) {
+               try {
+                       this.columns = columns;
+                       for (TabularColumn column : columns) {
+                               String normalized = JcrUtils.replaceInvalidChars(column
+                                               .getName());
+                               Node columnNode = tableNode.addNode(normalized,
+                                               ArgeoTypes.ARGEO_COLUMN);
+                               columnNode.setProperty(Property.JCR_TITLE, column.getName());
+                               if (column.getType() != null)
+                                       columnNode.setProperty(Property.JCR_REQUIRED_TYPE,
+                                                       PropertyType.nameFromValue(column.getType()));
+                               else
+                                       columnNode.setProperty(Property.JCR_REQUIRED_TYPE,
+                                                       PropertyType.TYPENAME_STRING);
+                       }
+                       contentNode = tableNode.addNode(Property.JCR_CONTENT,
+                                       contentNodeType);
+                       if (contentNodeType.equals(ArgeoTypes.ARGEO_CSV)) {
+                               contentNode.setProperty(Property.JCR_MIMETYPE, "text/csv");
+                               contentNode.setProperty(Property.JCR_ENCODING, "UTF-8");
+                               out = new ByteArrayOutputStream();
+                               csvWriter = new CsvWriter(out);
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot create table node " + tableNode, e);
+               }
+       }
+
+       public void appendRow(Object[] row) {
+               csvWriter.writeLine(row);
+       }
+
+       public void close() {
+               Binary binary = null;
+               InputStream in = null;
+               try {
+                       // TODO parallelize with pipes and writing from another thread
+                       in = new ByteArrayInputStream(out.toByteArray());
+                       binary = contentNode.getSession().getValueFactory()
+                                       .createBinary(in);
+                       contentNode.setProperty(Property.JCR_DATA, binary);
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot store data in " + contentNode, e);
+               } finally {
+                       IOUtils.closeQuietly(in);
+                       JcrUtils.closeQuietly(binary);
+               }
+       }
+}
diff --git a/org.argeo.eclipse.ui.rap/.classpath b/org.argeo.eclipse.ui.rap/.classpath
new file mode 100644 (file)
index 0000000..457b115
--- /dev/null
@@ -0,0 +1,9 @@
+<?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-1.8" />
+       <classpathentry kind="output" path="bin" />
+</classpath>
diff --git a/org.argeo.eclipse.ui.rap/.gitignore b/org.argeo.eclipse.ui.rap/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.eclipse.ui.rap/.project b/org.argeo.eclipse.ui.rap/.project
new file mode 100644 (file)
index 0000000..df496c2
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.eclipse.ui.rap</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.eclipse.ui.rap/META-INF/.gitignore b/org.argeo.eclipse.ui.rap/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.eclipse.ui.rap/bnd.bnd b/org.argeo.eclipse.ui.rap/bnd.bnd
new file mode 100644 (file)
index 0000000..9c15f0c
--- /dev/null
@@ -0,0 +1,4 @@
+Import-Package: org.eclipse.swt,\
+ org.eclipse.jface.dialogs,\
+ org.argeo.eclipse.ui.utils,\
+*
diff --git a/org.argeo.eclipse.ui.rap/build.properties b/org.argeo.eclipse.ui.rap/build.properties
new file mode 100644 (file)
index 0000000..fd806ca
--- /dev/null
@@ -0,0 +1,2 @@
+source.. = src/
+output.. = bin/
diff --git a/org.argeo.eclipse.ui.rap/pom.xml b/org.argeo.eclipse.ui.rap/pom.xml
new file mode 100644 (file)
index 0000000..49c317b
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>argeo-commons</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.eclipse.ui.rap</artifactId>
+       <name>Commons Eclipse UI RAP</name>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.util</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- UI -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.rwt</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.commands</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.jface</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/EclipseUiSpecificUtils.java b/org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/EclipseUiSpecificUtils.java
new file mode 100644 (file)
index 0000000..b669033
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.specific;
+
+import org.eclipse.jface.viewers.AbstractTableViewer;
+import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
+import org.eclipse.jface.viewers.Viewer;
+
+/** Static utilities to bridge differences between RCP and RAP */
+public class EclipseUiSpecificUtils {
+
+       /**
+        * TootlTip support is supported only for {@link AbstractTableViewer} in RAP
+        */
+       public static void enableToolTipSupport(Viewer viewer) {
+               if (viewer instanceof AbstractTableViewer)
+                       ColumnViewerToolTipSupport.enableFor((AbstractTableViewer) viewer);
+       }
+
+       private EclipseUiSpecificUtils() {
+       }
+}
diff --git a/org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/OpenFile.java b/org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/OpenFile.java
new file mode 100644 (file)
index 0000000..efc0733
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.specific;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.utils.SingleSourcingConstants;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.rap.rwt.client.service.UrlLauncher;
+
+/**
+ * RWT specific object to open a file retrieved from the server. It forwards the
+ * request to the correct service after encoding file name and path in the
+ * request URI.
+ * 
+ * <p>
+ * The parameter "URI" is used to determine the correct file service, the path
+ * and the file name. An optional file name can be added to present the end user
+ * with a different file name as the one used to retrieve it.
+ * </p>
+ * 
+ * 
+ * <p>
+ * The instance specific service is called by its ID and must have been
+ * externally created
+ * </p>
+ */
+public class OpenFile extends AbstractHandler {
+       private final static Log log = LogFactory.getLog(OpenFile.class);
+
+       public final static String ID = SingleSourcingConstants.OPEN_FILE_CMD_ID;
+       public final static String PARAM_FILE_NAME = SingleSourcingConstants.PARAM_FILE_NAME;
+       public final static String PARAM_FILE_URI = SingleSourcingConstants.PARAM_FILE_URI;;
+       
+       /* DEPENDENCY INJECTION */
+       private String openFileServiceId;
+
+       public Object execute(ExecutionEvent event) {
+               String fileName = event.getParameter(PARAM_FILE_NAME);
+               String fileUri = event.getParameter(PARAM_FILE_URI);
+               // Sanity check
+               if (fileUri == null || "".equals(fileUri.trim()) || openFileServiceId == null
+                               || "".equals(openFileServiceId.trim()))
+                       return null;
+
+               org.argeo.eclipse.ui.specific.OpenFile openFileClient = new org.argeo.eclipse.ui.specific.OpenFile();
+               openFileClient.execute(openFileServiceId, fileUri, fileName);
+               return null;
+       }
+
+       public Object execute(String openFileServiceId, String fileUri, String fileName) {
+               StringBuilder url = new StringBuilder();
+               url.append(RWT.getServiceManager().getServiceHandlerUrl(openFileServiceId));
+
+               if (EclipseUiUtils.notEmpty(fileName))
+                       url.append("&").append(SingleSourcingConstants.PARAM_FILE_NAME).append("=").append(fileName);
+               url.append("&").append(SingleSourcingConstants.PARAM_FILE_URI).append("=").append(fileUri);
+
+               String downloadUrl = url.toString();
+               if (log.isTraceEnabled())
+                       log.trace("Calling OpenFileService with ID: " + openFileServiceId + " , with download URL: " + downloadUrl);
+
+               UrlLauncher launcher = RWT.getClient().getService(UrlLauncher.class);
+               launcher.openURL(downloadUrl);
+               return null;
+       }
+
+       /* DEPENDENCY INJECTION */
+       public void setOpenFileServiceId(String openFileServiceId) {
+               this.openFileServiceId = openFileServiceId;
+       }
+}
diff --git a/org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/OpenFileService.java b/org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/OpenFileService.java
new file mode 100644 (file)
index 0000000..a181a29
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.specific;
+
+import static org.argeo.eclipse.ui.utils.SingleSourcingConstants.FILE_SCHEME;
+import static org.argeo.eclipse.ui.utils.SingleSourcingConstants.SCHEME_HOST_SEPARATOR;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.utils.SingleSourcingConstants;
+import org.eclipse.rap.rwt.service.ServiceHandler;
+
+/**
+ * RWT specific Basic Default service handler that retrieves a file on the
+ * server file system using its absolute path and forwards it to the end user
+ * browser.
+ * 
+ * Clients might extend to provide context specific services
+ */
+public class OpenFileService implements ServiceHandler {
+       public OpenFileService() {
+       }
+
+       public void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+               String fileName = request.getParameter(SingleSourcingConstants.PARAM_FILE_NAME);
+               String uri = request.getParameter(SingleSourcingConstants.PARAM_FILE_URI);
+
+               // Use buffered array to directly write the stream?
+               if (!uri.startsWith(SingleSourcingConstants.FILE_SCHEME))
+                       throw new IllegalArgumentException(
+                                       "Open file service can only handle files that are on the server file system");
+
+               // Set the Metadata
+               response.setContentLength((int) getFileSize(uri));
+               if (EclipseUiUtils.isEmpty(fileName))
+                       fileName = getFileName(uri);
+               response.setContentType(getMimeType(uri, fileName));
+               String contentDisposition = "attachment; filename=\"" + fileName + "\"";
+               response.setHeader("Content-Disposition", contentDisposition);
+
+               // Useless for current use
+               // response.setHeader("Content-Transfer-Encoding", "binary");
+               // response.setHeader("Pragma", "no-cache");
+               // response.setHeader("Cache-Control", "no-cache, must-revalidate");
+
+               Path path = Paths.get(getAbsPathFromUri(uri));
+               Files.copy(path, response.getOutputStream());
+
+               // FIXME we always use temporary files for the time being.
+               // the deleteOnClose file only works when the JVM is closed so we
+               // explicitly delete to avoid overloading the server
+               if (path.startsWith("/tmp"))
+                       path.toFile().delete();
+       }
+
+       protected long getFileSize(String uri) throws IOException {
+               if (uri.startsWith(SingleSourcingConstants.FILE_SCHEME)) {
+                       Path path = Paths.get(getAbsPathFromUri(uri));
+                       return Files.size(path);
+               }
+               return -1l;
+       }
+
+       protected String getFileName(String uri) {
+               if (uri.startsWith(SingleSourcingConstants.FILE_SCHEME)) {
+                       Path path = Paths.get(getAbsPathFromUri(uri));
+                       return path.getFileName().toString();
+               }
+               return null;
+       }
+
+       private String getAbsPathFromUri(String uri) {
+               if (uri.startsWith(FILE_SCHEME))
+                       return uri.substring((FILE_SCHEME + SCHEME_HOST_SEPARATOR).length());
+               // else if (uri.startsWith(JCR_SCHEME))
+               // return uri.substring((JCR_SCHEME + SCHEME_HOST_SEPARATOR).length());
+               else
+                       throw new IllegalArgumentException("Unknown URI prefix for" + uri);
+       }
+
+       protected String getMimeType(String uri, String fileName) throws IOException {
+               if (uri.startsWith(FILE_SCHEME)) {
+                       Path path = Paths.get(getAbsPathFromUri(uri));
+                       String mimeType = Files.probeContentType(path);
+                       if (EclipseUiUtils.notEmpty(mimeType))
+                               return mimeType;
+               }
+               return "application/octet-stream";
+       }
+}
diff --git a/org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/SingleSourcingException.java b/org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/SingleSourcingException.java
new file mode 100644 (file)
index 0000000..9b75690
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.eclipse.ui.specific;
+
+/** Exception related to SWT/RWT single sourcing. */
+public class SingleSourcingException extends RuntimeException {
+       private static final long serialVersionUID = -727700418055348468L;
+
+       public SingleSourcingException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+       public SingleSourcingException(String message) {
+               super(message);
+       }
+
+}
diff --git a/org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/UiContext.java b/org.argeo.eclipse.ui.rap/src/org/argeo/eclipse/ui/specific/UiContext.java
new file mode 100644 (file)
index 0000000..dac2700
--- /dev/null
@@ -0,0 +1,59 @@
+package org.argeo.eclipse.ui.specific;
+
+import java.util.Locale;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.rap.rwt.RWT;
+import org.eclipse.swt.widgets.Display;
+
+/** Singleton class providing single sources infos about the UI context. */
+public class UiContext {
+       /** Can be null, thus indicating that we are not in a web context. */
+       public static HttpServletRequest getHttpRequest() {
+               return RWT.getRequest();
+       }
+
+       public static HttpServletResponse getHttpResponse() {
+               return RWT.getResponse();
+       }
+
+       public static Locale getLocale() {
+               if (Display.getCurrent() != null)
+                       return RWT.getUISession().getLocale();
+               else
+                       return Locale.getDefault();
+       }
+
+       public static void setLocale(Locale locale) {
+               if (Display.getCurrent() != null)
+                       RWT.getUISession().setLocale(locale);
+               else
+                       Locale.setDefault(locale);
+       }
+
+       /** Can always be null */
+       @SuppressWarnings("unchecked")
+       public static <T> T getData(String key) {
+               Display display = getDisplay();
+               if (display == null)
+                       return null;
+               return (T) display.getData(key);
+       }
+
+       public static void setData(String key, Object value) {
+               Display display = getDisplay();
+               if (display == null)
+                       throw new SingleSourcingException("Not display available in RAP context");
+               display.setData(key, value);
+       }
+
+       private static Display getDisplay() {
+               return Display.getCurrent();
+       }
+
+       private UiContext() {
+       }
+
+}
diff --git a/org.argeo.eclipse.ui/.classpath b/org.argeo.eclipse.ui/.classpath
new file mode 100644 (file)
index 0000000..457b115
--- /dev/null
@@ -0,0 +1,9 @@
+<?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-1.8" />
+       <classpathentry kind="output" path="bin" />
+</classpath>
diff --git a/org.argeo.eclipse.ui/.gitignore b/org.argeo.eclipse.ui/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.eclipse.ui/.project b/org.argeo.eclipse.ui/.project
new file mode 100644 (file)
index 0000000..3140a5c
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.eclipse.ui</name>
+       <comment></comment>
+       <projects></projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments />
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments />
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments />
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/META-INF/.gitignore b/org.argeo.eclipse.ui/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.eclipse.ui/bnd.bnd b/org.argeo.eclipse.ui/bnd.bnd
new file mode 100644 (file)
index 0000000..b836fc0
--- /dev/null
@@ -0,0 +1,5 @@
+Import-Package: javax.jcr.nodetype,\
+                               org.eclipse.swt,\
+                               org.eclipse.jface.window,\
+                               org.eclipse.core.commands.common,\
+                               *
diff --git a/org.argeo.eclipse.ui/build.properties b/org.argeo.eclipse.ui/build.properties
new file mode 100644 (file)
index 0000000..0e04387
--- /dev/null
@@ -0,0 +1,5 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
+               
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/pom.xml b/org.argeo.eclipse.ui/pom.xml
new file mode 100644 (file)
index 0000000..887666b
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>argeo-commons</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.eclipse.ui</artifactId>
+       <name>Commons Eclipse UI</name>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.util</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.jcr</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- UI -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.rwt</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.core.commands</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.rap.jface</artifactId>
+                       <scope>provided</scope>
+               </dependency>
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/AbstractTreeContentProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/AbstractTreeContentProvider.java
new file mode 100644 (file)
index 0000000..9174b51
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Tree content provider dealing with tree objects and providing reasonable
+ * defaults.
+ */
+public abstract class AbstractTreeContentProvider implements
+               ITreeContentProvider {
+       private static final long serialVersionUID = 8246126401957763868L;
+
+       /** Does nothing */
+       public void dispose() {
+       }
+
+       /** Does nothing */
+       public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+       }
+
+       public Object[] getChildren(Object element) {
+               if (element instanceof TreeParent) {
+                       return ((TreeParent) element).getChildren();
+               }
+               return new Object[0];
+       }
+
+       public Object getParent(Object element) {
+               if (element instanceof TreeParent) {
+                       return ((TreeParent) element).getParent();
+               }
+               return null;
+       }
+
+       public boolean hasChildren(Object element) {
+               if (element instanceof TreeParent) {
+                       return ((TreeParent) element).hasChildren();
+               }
+               return false;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/ColumnDefinition.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/ColumnDefinition.java
new file mode 100644 (file)
index 0000000..a38552c
--- /dev/null
@@ -0,0 +1,68 @@
+package org.argeo.eclipse.ui;
+
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+
+/**
+ * Wraps the definition of a column to be used in the various JFace viewers
+ * (typically tree and table). It enables definition of generic viewers which
+ * column can be then defined externally. Also used to generate export.
+ */
+public class ColumnDefinition {
+       private ColumnLabelProvider labelProvider;
+       private String label;
+       private int weight = 0;
+       private int minWidth = 120;
+
+       public ColumnDefinition(ColumnLabelProvider labelProvider, String label) {
+               this.labelProvider = labelProvider;
+               this.label = label;
+       }
+
+       public ColumnDefinition(ColumnLabelProvider labelProvider, String label,
+                       int weight) {
+               this.labelProvider = labelProvider;
+               this.label = label;
+               this.weight = weight;
+               this.minWidth = weight;
+       }
+
+       public ColumnDefinition(ColumnLabelProvider labelProvider, String label,
+                       int weight, int minimumWidth) {
+               this.labelProvider = labelProvider;
+               this.label = label;
+               this.weight = weight;
+               this.minWidth = minimumWidth;
+       }
+
+       public ColumnLabelProvider getLabelProvider() {
+               return labelProvider;
+       }
+
+       public void setLabelProvider(ColumnLabelProvider labelProvider) {
+               this.labelProvider = labelProvider;
+       }
+
+       public String getLabel() {
+               return label;
+       }
+
+       public void setLabel(String label) {
+               this.label = label;
+       }
+
+       public int getWeight() {
+               return weight;
+       }
+
+       public void setWeight(int weight) {
+               this.weight = weight;
+       }
+
+       public int getMinWidth() {
+               return minWidth;
+       }
+
+       public void setMinWidth(int minWidth) {
+               this.minWidth = minWidth;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/ColumnViewerComparator.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/ColumnViewerComparator.java
new file mode 100644 (file)
index 0000000..f13378a
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui;
+
+import org.eclipse.jface.viewers.ColumnViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerComparator;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+
+/** Generic column viewer sorter */
+public class ColumnViewerComparator extends ViewerComparator {
+       private static final long serialVersionUID = -2266218906355859909L;
+
+       public static final int ASC = 1;
+
+       public static final int NONE = 0;
+
+       public static final int DESC = -1;
+
+       private int direction = 0;
+
+       private TableViewerColumn column;
+
+       private ColumnViewer viewer;
+
+       public ColumnViewerComparator(TableViewerColumn column) {
+               super(null);
+               this.column = column;
+               this.viewer = column.getViewer();
+               this.column.getColumn().addSelectionListener(new SelectionAdapter() {
+                       private static final long serialVersionUID = 7586796298965472189L;
+
+                       public void widgetSelected(SelectionEvent e) {
+                               if (ColumnViewerComparator.this.viewer.getComparator() != null) {
+                                       if (ColumnViewerComparator.this.viewer.getComparator() == ColumnViewerComparator.this) {
+                                               int tdirection = ColumnViewerComparator.this.direction;
+
+                                               if (tdirection == ASC) {
+                                                       setSortDirection(DESC);
+                                               } else if (tdirection == DESC) {
+                                                       setSortDirection(NONE);
+                                               }
+                                       } else {
+                                               setSortDirection(ASC);
+                                       }
+                               } else {
+                                       setSortDirection(ASC);
+                               }
+                       }
+               });
+       }
+
+       private void setSortDirection(int direction) {
+               if (direction == NONE) {
+                       column.getColumn().getParent().setSortColumn(null);
+                       column.getColumn().getParent().setSortDirection(SWT.NONE);
+                       viewer.setComparator(null);
+               } else {
+                       column.getColumn().getParent().setSortColumn(column.getColumn());
+                       this.direction = direction;
+
+                       if (direction == ASC) {
+                               column.getColumn().getParent().setSortDirection(SWT.DOWN);
+                       } else {
+                               column.getColumn().getParent().setSortDirection(SWT.UP);
+                       }
+
+                       if (viewer.getComparator() == this) {
+                               viewer.refresh();
+                       } else {
+                               viewer.setComparator(this);
+                       }
+
+               }
+       }
+
+       public int compare(Viewer viewer, Object e1, Object e2) {
+               return direction * super.compare(viewer, e1, e2);
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/EclipseJcrMonitor.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/EclipseJcrMonitor.java
new file mode 100644 (file)
index 0000000..d9cdd83
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui;
+
+import org.argeo.jcr.JcrMonitor;
+import org.eclipse.core.runtime.IProgressMonitor;
+
+/**
+ * Wraps an Eclipse {@link IProgressMonitor} so that it can be passed to
+ * framework agnostic Argeo routines.
+ */
+public class EclipseJcrMonitor implements JcrMonitor {
+       private final IProgressMonitor progressMonitor;
+
+       public EclipseJcrMonitor(IProgressMonitor progressMonitor) {
+               this.progressMonitor = progressMonitor;
+       }
+
+       public void beginTask(String name, int totalWork) {
+               progressMonitor.beginTask(name, totalWork);
+       }
+
+       public void done() {
+               progressMonitor.done();
+       }
+
+       public boolean isCanceled() {
+               return progressMonitor.isCanceled();
+       }
+
+       public void setCanceled(boolean value) {
+               progressMonitor.setCanceled(value);
+       }
+
+       public void setTaskName(String name) {
+               progressMonitor.setTaskName(name);
+       }
+
+       public void subTask(String name) {
+               progressMonitor.subTask(name);
+       }
+
+       public void worked(int work) {
+               progressMonitor.worked(work);
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/EclipseUiException.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/EclipseUiException.java
new file mode 100644 (file)
index 0000000..37a36e8
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.eclipse.ui;
+
+/** CMS specific exceptions. */
+public class EclipseUiException extends RuntimeException {
+       private static final long serialVersionUID = -5341764743356771313L;
+
+       public EclipseUiException(String message) {
+               super(message);
+       }
+
+       public EclipseUiException(String message, Throwable e) {
+               super(message, e);
+       }
+
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/EclipseUiUtils.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/EclipseUiUtils.java
new file mode 100644 (file)
index 0000000..8a628c8
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui;
+
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/** Utilities to simplify UI development. */
+public class EclipseUiUtils {
+
+       /** Dispose all children of a Composite */
+       public static void clear(Composite composite) {
+               for (Control child : composite.getChildren())
+                       child.dispose();
+       }
+
+       /**
+        * Enables efficient call to the layout method of a composite, refreshing only
+        * some of the children controls.
+        */
+       public static void layout(Composite parent, Control... toUpdateControls) {
+               parent.layout(toUpdateControls);
+       }
+
+       //
+       // FONTS
+       //
+       /** Shortcut to retrieve default italic font from display */
+       public static Font getItalicFont(Composite parent) {
+               return JFaceResources.getFontRegistry().defaultFontDescriptor().setStyle(SWT.ITALIC)
+                               .createFont(parent.getDisplay());
+       }
+
+       /** Shortcut to retrieve default bold font from display */
+       public static Font getBoldFont(Composite parent) {
+               return JFaceResources.getFontRegistry().defaultFontDescriptor().setStyle(SWT.BOLD)
+                               .createFont(parent.getDisplay());
+       }
+
+       /** Shortcut to retrieve default bold italic font from display */
+       public static Font getBoldItalicFont(Composite parent) {
+               return JFaceResources.getFontRegistry().defaultFontDescriptor().setStyle(SWT.BOLD | SWT.ITALIC)
+                               .createFont(parent.getDisplay());
+       }
+
+       //
+       // Simplify grid layouts management
+       //
+       public static GridLayout noSpaceGridLayout() {
+               return noSpaceGridLayout(new GridLayout());
+       }
+
+       public static GridLayout noSpaceGridLayout(int columns) {
+               return noSpaceGridLayout(new GridLayout(columns, false));
+       }
+
+       public static GridLayout noSpaceGridLayout(GridLayout layout) {
+               layout.horizontalSpacing = 0;
+               layout.verticalSpacing = 0;
+               layout.marginWidth = 0;
+               layout.marginHeight = 0;
+               return layout;
+       }
+
+       public static GridData fillWidth() {
+               return grabWidth(SWT.FILL, SWT.FILL);
+       }
+
+       public static GridData fillWidth(int colSpan) {
+               GridData gd = grabWidth(SWT.FILL, SWT.FILL);
+               gd.horizontalSpan = colSpan;
+               return gd;
+       }
+
+       public static GridData fillAll() {
+               return new GridData(SWT.FILL, SWT.FILL, true, true);
+       }
+
+       public static GridData fillAll(int colSpan, int rowSpan) {
+               return new GridData(SWT.FILL, SWT.FILL, true, true, colSpan, rowSpan);
+       }
+
+       public static GridData grabWidth(int horizontalAlignment, int verticalAlignment) {
+               return new GridData(horizontalAlignment, horizontalAlignment, true, false);
+       }
+
+       //
+       // Simplify Form layout management
+       //
+
+       /**
+        * Creates a basic form data that is attached to the 4 corners of the parent
+        * composite
+        */
+       public static FormData fillFormData() {
+               FormData formData = new FormData();
+               formData.top = new FormAttachment(0, 0);
+               formData.left = new FormAttachment(0, 0);
+               formData.right = new FormAttachment(100, 0);
+               formData.bottom = new FormAttachment(100, 0);
+               return formData;
+       }
+
+       /**
+        * Create a label and a text field for a grid layout, the text field grabbing
+        * excess horizontal
+        * 
+        * @param parent
+        *            the parent composite
+        * @param label
+        *            the label to display
+        * @param modifyListener
+        *            a {@link ModifyListener} to listen on events on the text, can be
+        *            null
+        * @return the created text
+        * 
+        */
+       // FIXME why was this deprecated.
+       // * @ deprecated use { @ link #createGridLT(Composite, String)} instead
+       // @ Deprecated
+       public static Text createGridLT(Composite parent, String label, ModifyListener modifyListener) {
+               Label lbl = new Label(parent, SWT.LEAD);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               Text txt = new Text(parent, SWT.LEAD | SWT.BORDER);
+               txt.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               if (modifyListener != null)
+                       txt.addModifyListener(modifyListener);
+               return txt;
+       }
+
+       /**
+        * Create a label and a text field for a grid layout, the text field grabbing
+        * excess horizontal
+        */
+       public static Text createGridLT(Composite parent, String label) {
+               return createGridLT(parent, label, null);
+       }
+
+       /**
+        * Creates one label and a text field not editable with background colour of the
+        * parent (like a label but with selectable text)
+        */
+       public static Text createGridLL(Composite parent, String label, String text) {
+               Text txt = createGridLT(parent, label);
+               txt.setText(text);
+               txt.setEditable(false);
+               txt.setBackground(parent.getBackground());
+               return txt;
+       }
+
+       /**
+        * Create a label and a text field with password display for a grid layout, the
+        * text field grabbing excess horizontal
+        */
+       public static Text createGridLP(Composite parent, String label) {
+               return createGridLP(parent, label, null);
+       }
+
+       /**
+        * Create a label and a text field with password display for a grid layout, the
+        * text field grabbing excess horizontal. The given modify listener will be
+        * added to the newly created text field if not null.
+        */
+       public static Text createGridLP(Composite parent, String label, ModifyListener modifyListener) {
+               Label lbl = new Label(parent, SWT.LEAD);
+               lbl.setText(label);
+               lbl.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
+               Text txt = new Text(parent, SWT.LEAD | SWT.BORDER | SWT.PASSWORD);
+               txt.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+               if (modifyListener != null)
+                       txt.addModifyListener(modifyListener);
+               return txt;
+       }
+
+       // MISCELLANEOUS
+
+       /** Simply checks if a string is not null nor empty */
+       public static boolean notEmpty(String stringToTest) {
+               return !(stringToTest == null || "".equals(stringToTest.trim()));
+       }
+
+       /** Simply checks if a string is null or empty */
+       public static boolean isEmpty(String stringToTest) {
+               return stringToTest == null || "".equals(stringToTest.trim());
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/FileProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/FileProvider.java
new file mode 100644 (file)
index 0000000..91ca719
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui;
+
+import java.io.InputStream;
+
+/**
+ * Used for file download : subclasses must implement model specific methods to
+ * get a byte array representing a file given is ID.
+ */
+@Deprecated
+public interface FileProvider {
+
+       public byte[] getByteArrayFileFromId(String fileId);
+
+       public InputStream getInputStreamFromFileId(String fileId);
+
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/GenericTableComparator.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/GenericTableComparator.java
new file mode 100644 (file)
index 0000000..c6e205c
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui;
+
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerComparator;
+
+public abstract class GenericTableComparator extends ViewerComparator {
+       private static final long serialVersionUID = -1175894935075325810L;
+       protected int propertyIndex;
+       public static final int ASCENDING = 0, DESCENDING = 1;
+       protected int direction = DESCENDING;
+
+       /**
+        * Creates an instance of a sorter for TableViewer.
+        * 
+        * @param defaultColumnIndex
+        *            the default sorter column
+        */
+
+       public GenericTableComparator(int defaultColumnIndex, int direction) {
+               propertyIndex = defaultColumnIndex;
+               this.direction = direction;
+       }
+
+       public void setColumn(int column) {
+               if (column == this.propertyIndex) {
+                       // Same column as last sort; toggle the direction
+                       direction = 1 - direction;
+               } else {
+                       // New column; do a descending sort
+                       this.propertyIndex = column;
+                       direction = DESCENDING;
+               }
+       }
+
+       /**
+        * Must be Overriden in each view.
+        */
+       public abstract int compare(Viewer viewer, Object e1, Object e2);
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/IListProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/IListProvider.java
new file mode 100644 (file)
index 0000000..ac7b2d8
--- /dev/null
@@ -0,0 +1,20 @@
+package org.argeo.eclipse.ui;
+
+import java.util.List;
+
+/**
+ * Views and editors can implement this interface so that one of the list that
+ * is displayed in the part (For instance in a Table or a Tree Viewer) can be
+ * rebuilt externally. Typically to generate csv or calc extract.
+ */
+public interface IListProvider {
+       /**
+        * Returns an array of current and relevant elements
+        */
+       public Object[] getElements(String extractId);
+
+       /**
+        * Returns the column definition for passed ID
+        */
+       public List<ColumnDefinition> getColumnDefinition(String extractId);
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/Selected.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/Selected.java
new file mode 100644 (file)
index 0000000..4e95c8c
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.eclipse.ui;
+
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+
+/**
+ * {@link SelectionListener} as a functional interface in order to use lambda
+ * expression in UI code.
+ * {@link SelectionListener#widgetDefaultSelected(SelectionEvent)} does nothing
+ * by default.
+ */
+@FunctionalInterface
+public interface Selected extends SelectionListener {
+       @Override
+       public void widgetSelected(SelectionEvent e);
+
+       default public void widgetDefaultSelected(SelectionEvent e) {
+               // does nothing
+       }
+
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/TreeParent.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/TreeParent.java
new file mode 100644 (file)
index 0000000..2dfd2e6
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Parent / children semantic to be used for simple UI Tree structure */
+public class TreeParent {
+       private String name;
+       private TreeParent parent;
+
+       private List<Object> children;
+
+       /**
+        * Unique id within the context of a tree display. If set, equals() and
+        * hashCode() methods will be based on it
+        */
+       private String path = null;
+
+       /** False until at least one child has been added, then true until cleared */
+       private boolean loaded = false;
+
+       public TreeParent(String name) {
+               this.name = name;
+               children = new ArrayList<Object>();
+       }
+
+       public synchronized void addChild(Object child) {
+               loaded = true;
+               children.add(child);
+               if (child instanceof TreeParent)
+                       ((TreeParent) child).setParent(this);
+       }
+
+       /**
+        * Remove this child. The child is disposed.
+        */
+       public synchronized void removeChild(Object child) {
+               children.remove(child);
+               if (child instanceof TreeParent) {
+                       ((TreeParent) child).dispose();
+               }
+       }
+
+       public synchronized void clearChildren() {
+               for (Object obj : children) {
+                       if (obj instanceof TreeParent)
+                               ((TreeParent) obj).dispose();
+               }
+               loaded = false;
+               children.clear();
+       }
+
+       /**
+        * If overridden, <code>super.dispose()</code> must be called, typically
+        * after custom cleaning.
+        */
+       public synchronized void dispose() {
+               clearChildren();
+               parent = null;
+               children = null;
+       }
+
+       public synchronized Object[] getChildren() {
+               return children.toArray(new Object[children.size()]);
+       }
+
+       @SuppressWarnings("unchecked")
+       public synchronized <T> List<T> getChildrenOfType(Class<T> clss) {
+               List<T> lst = new ArrayList<T>();
+               for (Object obj : children) {
+                       if (clss.isAssignableFrom(obj.getClass()))
+                               lst.add((T) obj);
+               }
+               return lst;
+       }
+
+       public synchronized boolean hasChildren() {
+               return children.size() > 0;
+       }
+
+       public Object getChildByName(String name) {
+               for (Object child : children) {
+                       if (child.toString().equals(name))
+                               return child;
+               }
+               return null;
+       }
+
+       public synchronized Boolean isLoaded() {
+               return loaded;
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public void setParent(TreeParent parent) {
+               this.parent = parent;
+               if (parent != null && parent.path != null)
+                       this.path = parent.path + '/' + name;
+               else
+                       this.path = '/' + name;
+       }
+
+       public TreeParent getParent() {
+               return parent;
+       }
+
+       public String toString() {
+               return getName();
+       }
+
+       public int compareTo(TreeParent o) {
+               return name.compareTo(o.name);
+       }
+
+       @Override
+       public int hashCode() {
+               if (path != null)
+                       return path.hashCode();
+               else
+                       return name.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (path != null && obj instanceof TreeParent)
+                       return path.equals(((TreeParent) obj).path);
+               else
+                       return name.equals(obj.toString());
+       }
+
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/ErrorFeedback.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/ErrorFeedback.java
new file mode 100644 (file)
index 0000000..c311f01
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.dialogs;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/** Generic error dialog to be used in try/catch blocks */
+public class ErrorFeedback extends TitleAreaDialog {
+       private static final long serialVersionUID = -8918084784628179044L;
+
+       private final static Log log = LogFactory.getLog(ErrorFeedback.class);
+
+       private final String message;
+       private final Throwable exception;
+
+       public static void show(String message, Throwable e) {
+               // rethrow ThreaDeath in order to make sure that RAP will properly clean
+               // up the UI thread
+               if (e instanceof ThreadDeath)
+                       throw (ThreadDeath) e;
+
+               new ErrorFeedback(newShell(), message, e).open();
+       }
+
+       public static void show(String message) {
+               new ErrorFeedback(newShell(), message, null).open();
+       }
+
+       private static Shell newShell() {
+               return new Shell(getDisplay(), SWT.NO_TRIM);
+       }
+
+       /** Tries to find a display */
+       private static Display getDisplay() {
+               try {
+                       Display display = Display.getCurrent();
+                       if (display != null)
+                               return display;
+                       else
+                               return Display.getDefault();
+               } catch (Exception e) {
+                       return Display.getCurrent();
+               }
+       }
+
+       public ErrorFeedback(Shell parentShell, String message, Throwable e) {
+               super(parentShell);
+               setShellStyle(SWT.NO_TRIM);
+               this.message = message;
+               this.exception = e;
+               log.error(message, e);
+       }
+
+       protected Point getInitialSize() {
+               if (exception != null)
+                       return new Point(800, 600);
+               else
+                       return new Point(400, 300);
+       }
+
+       @Override
+       protected Control createDialogArea(Composite parent) {
+               Composite dialogarea = (Composite) super.createDialogArea(parent);
+               dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               Composite composite = new Composite(dialogarea, SWT.NONE);
+               composite.setLayout(new GridLayout(2, false));
+               composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               setMessage(message != null ? message + (exception != null ? ": " + exception.getMessage() : "")
+                               : exception != null ? exception.getMessage() : "Unkown Error", IMessageProvider.ERROR);
+
+               if (exception != null) {
+                       Text stack = new Text(composite, SWT.MULTI | SWT.LEAD | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+                       stack.setEditable(false);
+                       stack.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       StringWriter sw = new StringWriter();
+                       exception.printStackTrace(new PrintWriter(sw));
+                       stack.setText(sw.toString());
+               }
+
+               parent.pack();
+               return composite;
+       }
+
+       protected void configureShell(Shell shell) {
+               super.configureShell(shell);
+               shell.setText("Error");
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/FeedbackDialog.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/FeedbackDialog.java
new file mode 100644 (file)
index 0000000..1fd4340
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.dialogs;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/** Generic lightweight dialog, not based on JFace. */
+public class FeedbackDialog extends LightweightDialog {
+       private final static Log log = LogFactory.getLog(FeedbackDialog.class);
+
+       private String message;
+       private Throwable exception;
+
+       private Shell parentShell;
+       private Shell shell;
+
+       public static void show(String message, Throwable e) {
+               // rethrow ThreaDeath in order to make sure that RAP will properly clean
+               // up the UI thread
+               if (e instanceof ThreadDeath)
+                       throw (ThreadDeath) e;
+
+               new FeedbackDialog(getDisplay().getActiveShell(), message, e).open();
+       }
+
+       public static void show(String message) {
+               new FeedbackDialog(getDisplay().getActiveShell(), message, null).open();
+       }
+
+       /** Tries to find a display */
+       private static Display getDisplay() {
+               try {
+                       Display display = Display.getCurrent();
+                       if (display != null)
+                               return display;
+                       else
+                               return Display.getDefault();
+               } catch (Exception e) {
+                       return Display.getCurrent();
+               }
+       }
+
+       public FeedbackDialog(Shell parentShell, String message, Throwable e) {
+               super(parentShell);
+               this.message = message;
+               this.exception = e;
+               log.error(message, e);
+       }
+
+       public int open() {
+               if (shell != null)
+                       throw new EclipseUiException("There is already a shell");
+               shell = new Shell(getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+               shell.setLayout(new GridLayout());
+               // shell.setText("Error");
+               shell.setSize(getInitialSize());
+               createDialogArea(shell);
+               // shell.pack();
+               // shell.layout();
+
+               Rectangle shellBounds = Display.getCurrent().getBounds();// RAP
+               Point dialogSize = shell.getSize();
+               int x = shellBounds.x + (shellBounds.width - dialogSize.x) / 2;
+               int y = shellBounds.y + (shellBounds.height - dialogSize.y) / 2;
+               shell.setLocation(x, y);
+
+               shell.addShellListener(new ShellAdapter() {
+                       private static final long serialVersionUID = -2701270481953688763L;
+
+                       @Override
+                       public void shellDeactivated(ShellEvent e) {
+                               closeShell();
+                       }
+               });
+
+               shell.open();
+               return OK;
+       }
+
+       protected void closeShell() {
+               shell.close();
+               shell.dispose();
+               shell = null;
+       }
+
+       protected Point getInitialSize() {
+               // if (exception != null)
+               // return new Point(800, 600);
+               // else
+               return new Point(400, 300);
+       }
+
+       protected Control createDialogArea(Composite parent) {
+               Composite dialogarea = new Composite(parent, SWT.NONE);
+               dialogarea.setLayout(new GridLayout());
+               // Composite dialogarea = (Composite) super.createDialogArea(parent);
+               dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               Label messageLbl = new Label(dialogarea, SWT.NONE);
+               if (message != null)
+                       messageLbl.setText(message);
+               else if (exception != null)
+                       messageLbl.setText(exception.getLocalizedMessage());
+
+               Composite composite = new Composite(dialogarea, SWT.NONE);
+               composite.setLayout(new GridLayout(2, false));
+               composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               if (exception != null) {
+                       Text stack = new Text(composite, SWT.MULTI | SWT.LEAD | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+                       stack.setEditable(false);
+                       stack.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       StringWriter sw = new StringWriter();
+                       exception.printStackTrace(new PrintWriter(sw));
+                       stack.setText(sw.toString());
+               }
+
+               // parent.pack();
+               return composite;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/LightweightDialog.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/LightweightDialog.java
new file mode 100644 (file)
index 0000000..5649a15
--- /dev/null
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.dialogs;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusEvent;
+import org.eclipse.swt.events.FocusListener;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+/** Generic lightweight dialog, not based on JFace. */
+public class LightweightDialog {
+       private final static Log log = LogFactory.getLog(LightweightDialog.class);
+
+       // must be the same value as org.eclipse.jface.window.Window#OK
+       public final static int OK = 0;
+       // must be the same value as org.eclipse.jface.window.Window#CANCEL
+       public final static int CANCEL = 1;
+
+       private Shell parentShell;
+       private Shell backgroundShell;
+       private Shell foregoundShell;
+
+       private Integer returnCode = null;
+       private boolean block = true;
+
+       private String title;
+
+       /** Tries to find a display */
+       private static Display getDisplay() {
+               try {
+                       Display display = Display.getCurrent();
+                       if (display != null)
+                               return display;
+                       else
+                               return Display.getDefault();
+               } catch (Exception e) {
+                       return Display.getCurrent();
+               }
+       }
+
+       public LightweightDialog(Shell parentShell) {
+               this.parentShell = parentShell;
+       }
+
+       public int open() {
+               if (foregoundShell != null)
+                       throw new EclipseUiException("There is already a shell");
+               backgroundShell = new Shell(parentShell, SWT.ON_TOP);
+               backgroundShell.setFullScreen(true);
+               // if (parentShell != null) {
+               // backgroundShell.setBounds(parentShell.getBounds());
+               // } else
+               // backgroundShell.setMaximized(true);
+               backgroundShell.setAlpha(128);
+               backgroundShell.setBackground(getDisplay().getSystemColor(SWT.COLOR_BLACK));
+               foregoundShell = new Shell(backgroundShell, SWT.NO_TRIM | SWT.ON_TOP);
+               if (title != null)
+                       setTitle(title);
+               foregoundShell.setLayout(new GridLayout());
+               foregoundShell.setSize(getInitialSize());
+               createDialogArea(foregoundShell);
+               // shell.pack();
+               // shell.layout();
+
+               Rectangle shellBounds = parentShell != null ? parentShell.getBounds() : Display.getCurrent().getBounds();// RAP
+               Point dialogSize = foregoundShell.getSize();
+               int x = shellBounds.x + (shellBounds.width - dialogSize.x) / 2;
+               int y = shellBounds.y + (shellBounds.height - dialogSize.y) / 2;
+               foregoundShell.setLocation(x, y);
+
+               foregoundShell.addShellListener(new ShellAdapter() {
+                       private static final long serialVersionUID = -2701270481953688763L;
+
+                       @Override
+                       public void shellDeactivated(ShellEvent e) {
+                               if (hasChildShells())
+                                       return;
+                               if (returnCode == null)// not yet closed
+                                       closeShell(CANCEL);
+                       }
+
+                       @Override
+                       public void shellClosed(ShellEvent e) {
+                               notifyClose();
+                       }
+
+               });
+
+               backgroundShell.open();
+               foregoundShell.open();
+               // after the foreground shell has been opened
+               backgroundShell.addFocusListener(new FocusListener() {
+                       private static final long serialVersionUID = 3137408447474661070L;
+
+                       @Override
+                       public void focusLost(FocusEvent event) {
+                       }
+
+                       @Override
+                       public void focusGained(FocusEvent event) {
+                               if (hasChildShells())
+                                       return;
+                               if (returnCode == null)// not yet closed
+                                       closeShell(CANCEL);
+                       }
+               });
+
+               if (block) {
+                       try {
+                               runEventLoop(foregoundShell);
+                       } catch (ThreadDeath t) {
+                               returnCode = CANCEL;
+                               if (log.isTraceEnabled())
+                                       log.error("Thread death, canceling dialog", t);
+                       } catch (Throwable t) {
+                               returnCode = CANCEL;
+                               log.error("Cannot open blocking lightweight dialog", t);
+                       }
+               }
+               if (returnCode == null)
+                       returnCode = OK;
+               return returnCode;
+       }
+
+       private boolean hasChildShells() {
+               if (foregoundShell == null)
+                       return false;
+               return foregoundShell.getShells().length != 0;
+       }
+
+       // public synchronized int openAndWait() {
+       // open();
+       // while (returnCode == null)
+       // try {
+       // wait(100);
+       // } catch (InterruptedException e) {
+       // // silent
+       // }
+       // return returnCode;
+       // }
+
+       private synchronized void notifyClose() {
+               if (returnCode == null)
+                       returnCode = CANCEL;
+               notifyAll();
+       }
+
+       protected void closeShell(int returnCode) {
+               this.returnCode = returnCode;
+               if (CANCEL == returnCode)
+                       onCancel();
+               if (foregoundShell != null && !foregoundShell.isDisposed()) {
+                       foregoundShell.close();
+                       foregoundShell.dispose();
+                       foregoundShell = null;
+               }
+
+               if (backgroundShell != null && !backgroundShell.isDisposed()) {
+                       backgroundShell.close();
+                       backgroundShell.dispose();
+               }
+       }
+
+       protected Point getInitialSize() {
+               // if (exception != null)
+               // return new Point(800, 600);
+               // else
+               return new Point(600, 400);
+       }
+
+       protected Control createDialogArea(Composite parent) {
+               Composite dialogarea = new Composite(parent, SWT.NONE);
+               dialogarea.setLayout(new GridLayout());
+               dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               return dialogarea;
+       }
+
+       protected Shell getBackgroundShell() {
+               return backgroundShell;
+       }
+
+       protected Shell getForegoundShell() {
+               return foregoundShell;
+       }
+
+       public void setBlockOnOpen(boolean shouldBlock) {
+               block = shouldBlock;
+       }
+
+       public void pack() {
+               foregoundShell.pack();
+       }
+
+       private void runEventLoop(Shell loopShell) {
+               Display display;
+               if (foregoundShell == null) {
+                       display = Display.getCurrent();
+               } else {
+                       display = loopShell.getDisplay();
+               }
+
+               while (loopShell != null && !loopShell.isDisposed()) {
+                       try {
+                               if (!display.readAndDispatch()) {
+                                       display.sleep();
+                               }
+                       } catch (Throwable e) {
+                               handleException(e);
+                       }
+               }
+               if (!display.isDisposed())
+                       display.update();
+       }
+
+       protected void handleException(Throwable t) {
+               if (t instanceof ThreadDeath) {
+                       // Don't catch ThreadDeath as this is a normal occurrence when
+                       // the thread dies
+                       throw (ThreadDeath) t;
+               }
+               // Try to keep running.
+               t.printStackTrace();
+       }
+
+       /** @return false, if the dialog should not be closed. */
+       protected boolean onCancel() {
+               return true;
+       }
+
+       public void setTitle(String title) {
+               this.title = title;
+               if (title != null && getForegoundShell() != null)
+                       getForegoundShell().setText(title);
+       }
+
+       public Integer getReturnCode() {
+               return returnCode;
+       }
+
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/NonModalErrorFeedback.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/NonModalErrorFeedback.java
new file mode 100644 (file)
index 0000000..513a248
--- /dev/null
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.dialogs;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/** Generic error dialog to be used in try/catch blocks */
+class NonModalErrorFeedback {
+       private final static Log log = LogFactory
+                       .getLog(NonModalErrorFeedback.class);
+
+       private final String message;
+       private final Throwable exception;
+
+       private Shell shell;
+
+       public static void show(String message, Throwable e) {
+               // rethrow ThreaDeath in order to make sure that RAP will properly clean
+               // up the UI thread
+               if (e instanceof ThreadDeath)
+                       throw (ThreadDeath) e;
+
+               new NonModalErrorFeedback(getDisplay().getActiveShell(), message, e)
+                               .open();
+       }
+
+       public static void show(String message) {
+               new NonModalErrorFeedback(getDisplay().getActiveShell(), message, null)
+                               .open();
+       }
+
+       /** Tries to find a display */
+       private static Display getDisplay() {
+               try {
+                       Display display = Display.getCurrent();
+                       if (display != null)
+                               return display;
+                       else
+                               return Display.getDefault();
+               } catch (Exception e) {
+                       return Display.getCurrent();
+               }
+       }
+
+       public NonModalErrorFeedback(Shell parentShell, String message, Throwable e) {
+               this.message = message;
+               this.exception = e;
+               log.error(message, e);
+       }
+
+       public void open() {
+               if (shell != null)
+                       throw new EclipseUiException("There is already a shell");
+               shell = new Shell(getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP);
+               shell.setLayout(new GridLayout());
+               // shell.setText("Error");
+               shell.setSize(getInitialSize());
+               createDialogArea(shell);
+               // shell.pack();
+               // shell.layout();
+
+               Rectangle shellBounds = Display.getCurrent().getBounds();// RAP
+               Point dialogSize = shell.getSize();
+               int x = shellBounds.x + (shellBounds.width - dialogSize.x) / 2;
+               int y = shellBounds.y + (shellBounds.height - dialogSize.y) / 2;
+               shell.setLocation(x, y);
+
+               shell.addShellListener(new ShellAdapter() {
+                       private static final long serialVersionUID = -2701270481953688763L;
+
+                       @Override
+                       public void shellDeactivated(ShellEvent e) {
+                               closeShell();
+                       }
+               });
+
+               shell.open();
+       }
+
+       protected void closeShell() {
+               shell.close();
+               shell.dispose();
+               shell = null;
+       }
+
+       protected Point getInitialSize() {
+               // if (exception != null)
+               // return new Point(800, 600);
+               // else
+               return new Point(400, 300);
+       }
+
+       protected Control createDialogArea(Composite parent) {
+               Composite dialogarea = new Composite(parent, SWT.NONE);
+               dialogarea.setLayout(new GridLayout());
+               // Composite dialogarea = (Composite) super.createDialogArea(parent);
+               dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               Label messageLbl = new Label(dialogarea, SWT.NONE);
+               if (message != null)
+                       messageLbl.setText(message);
+               else if (exception != null)
+                       messageLbl.setText(exception.getLocalizedMessage());
+
+               Composite composite = new Composite(dialogarea, SWT.NONE);
+               composite.setLayout(new GridLayout(2, false));
+               composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+               if (exception != null) {
+                       Text stack = new Text(composite, SWT.MULTI | SWT.LEAD | SWT.BORDER
+                                       | SWT.V_SCROLL | SWT.H_SCROLL);
+                       stack.setEditable(false);
+                       stack.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+                       StringWriter sw = new StringWriter();
+                       exception.printStackTrace(new PrintWriter(sw));
+                       stack.setText(sw.toString());
+               }
+
+               // parent.pack();
+               return composite;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/SingleValue.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/SingleValue.java
new file mode 100644 (file)
index 0000000..8cce0e2
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.dialogs;
+
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/** Dialog to retrieve a single value. */
+public class SingleValue extends TitleAreaDialog {
+       private static final long serialVersionUID = 2843538207460082349L;
+
+       private Text valueT;
+       private String value;
+       private final String title, message, label;
+       private final Boolean multiline;
+
+       public static String ask(String label, String message) {
+               SingleValue svd = new SingleValue(label, message);
+               if (svd.open() == Window.OK)
+                       return svd.getString();
+               else
+                       return null;
+       }
+
+       public static Long askLong(String label, String message) {
+               SingleValue svd = new SingleValue(label, message);
+               if (svd.open() == Window.OK)
+                       return svd.getLong();
+               else
+                       return null;
+       }
+
+       public static Double askDouble(String label, String message) {
+               SingleValue svd = new SingleValue(label, message);
+               if (svd.open() == Window.OK)
+                       return svd.getDouble();
+               else
+                       return null;
+       }
+
+       public SingleValue(String label, String message) {
+               this(Display.getDefault().getActiveShell(), label, message, label,
+                               false);
+       }
+
+       public SingleValue(Shell parentShell, String title, String message,
+                       String label, Boolean multiline) {
+               super(parentShell);
+               this.title = title;
+               this.message = message;
+               this.label = label;
+               this.multiline = multiline;
+       }
+
+       protected Point getInitialSize() {
+               if (multiline)
+                       return new Point(450, 350);
+
+               else
+                       return new Point(400, 270);
+       }
+
+       protected Control createDialogArea(Composite parent) {
+               Composite dialogarea = (Composite) super.createDialogArea(parent);
+               dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               Composite composite = new Composite(dialogarea, SWT.NONE);
+               composite.setLayoutData(EclipseUiUtils.fillAll());
+               GridLayout layout = new GridLayout(2, false);
+               layout.marginWidth = layout.marginHeight = 20;
+               composite.setLayout(layout);
+
+               valueT = createLT(composite, label);
+
+               setMessage(message, IMessageProvider.NONE);
+
+               parent.pack();
+               valueT.setFocus();
+               return composite;
+       }
+
+       @Override
+       protected void okPressed() {
+               value = valueT.getText();
+               super.okPressed();
+       }
+
+       /** Creates label and text. */
+       protected Text createLT(Composite parent, String label) {
+               new Label(parent, SWT.NONE).setText(label);
+               Text text;
+               if (multiline) {
+                       text = new Text(parent, SWT.LEAD | SWT.BORDER | SWT.MULTI);
+                       text.setLayoutData(EclipseUiUtils.fillAll());
+               } else {
+                       text = new Text(parent, SWT.LEAD | SWT.BORDER | SWT.SINGLE);
+                       text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true));
+               }
+               return text;
+       }
+
+       protected void configureShell(Shell shell) {
+               super.configureShell(shell);
+               shell.setText(title);
+       }
+
+       public String getString() {
+               return value;
+       }
+
+       public Long getLong() {
+               return Long.valueOf(getString());
+       }
+
+       public Double getDouble() {
+               return Double.valueOf(getString());
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/AdvancedFsBrowser.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/AdvancedFsBrowser.java
new file mode 100644 (file)
index 0000000..136eb50
--- /dev/null
@@ -0,0 +1,451 @@
+package org.argeo.eclipse.ui.fs;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Text;
+
+/** Simple UI provider that populates a composite parent given a NIO path */
+public class AdvancedFsBrowser {
+       private final static Log log = LogFactory.getLog(AdvancedFsBrowser.class);
+
+       // Some local constants to experiment. should be cleaned
+       // private final static int THUMBNAIL_WIDTH = 400;
+       // private Point imageWidth = new Point(250, 0);
+       private final static int COLUMN_WIDTH = 160;
+
+       private Path initialPath;
+       private Path currEdited;
+       // Filter
+       private Composite displayBoxCmp;
+       private Text parentPathTxt;
+       private Text filterTxt;
+       // Browser columns
+       private ScrolledComposite scrolledCmp;
+       // Keep a cache of the opened directories
+       private LinkedHashMap<Path, FilterEntitiesVirtualTable> browserCols = new LinkedHashMap<>();
+       private Composite scrolledCmpBody;
+
+       public Control createUi(Composite parent, Path basePath) {
+               if (basePath == null)
+                       throw new IllegalArgumentException("Context cannot be null");
+               parent.setLayout(new GridLayout());
+
+               // top filter
+               Composite filterCmp = new Composite(parent, SWT.NO_FOCUS);
+               filterCmp.setLayoutData(EclipseUiUtils.fillWidth());
+               addFilterPanel(filterCmp);
+
+               // Bottom part a sash with browser on the left
+               SashForm form = new SashForm(parent, SWT.HORIZONTAL);
+               // form.setLayout(new FillLayout());
+               form.setLayoutData(EclipseUiUtils.fillAll());
+               Composite leftCmp = new Composite(form, SWT.NO_FOCUS);
+               displayBoxCmp = new Composite(form, SWT.NONE);
+               form.setWeights(new int[] { 3, 1 });
+
+               createBrowserPart(leftCmp, basePath);
+               // leftCmp.addControlListener(new ControlAdapter() {
+               // @Override
+               // public void controlResized(ControlEvent e) {
+               // Rectangle r = leftCmp.getClientArea();
+               // log.warn("Browser resized: " + r.toString());
+               // scrolledCmp.setMinSize(browserCols.size() * (COLUMN_WIDTH + 2),
+               // SWT.DEFAULT);
+               // // scrolledCmp.setMinSize(scrolledCmpBody.computeSize(SWT.DEFAULT,
+               // // r.height));
+               // }
+               // });
+
+               populateCurrEditedDisplay(displayBoxCmp, basePath);
+
+               // INIT
+               setEdited(basePath);
+               initialPath = basePath;
+               // form.layout(true, true);
+               return parent;
+       }
+
+       private void createBrowserPart(Composite parent, Path context) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+               // scrolled composite
+               scrolledCmp = new ScrolledComposite(parent, SWT.H_SCROLL | SWT.BORDER | SWT.NO_FOCUS);
+               scrolledCmp.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+               scrolledCmp.setExpandVertical(true);
+               scrolledCmp.setExpandHorizontal(true);
+               scrolledCmp.setShowFocusedControl(true);
+
+               scrolledCmpBody = new Composite(scrolledCmp, SWT.NO_FOCUS);
+               scrolledCmp.setContent(scrolledCmpBody);
+               scrolledCmpBody.addControlListener(new ControlAdapter() {
+                       private static final long serialVersionUID = 183238447102854553L;
+
+                       @Override
+                       public void controlResized(ControlEvent e) {
+                               Rectangle r = scrolledCmp.getClientArea();
+                               scrolledCmp.setMinSize(scrolledCmpBody.computeSize(SWT.DEFAULT, r.height));
+                       }
+               });
+               initExplorer(scrolledCmpBody, context);
+               scrolledCmpBody.layout(true, true);
+               scrolledCmp.layout();
+
+       }
+
+       private Control initExplorer(Composite parent, Path context) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+               return createBrowserColumn(parent, context);
+       }
+
+       private Control createBrowserColumn(Composite parent, Path context) {
+               // TODO style is not correctly managed.
+               FilterEntitiesVirtualTable table = new FilterEntitiesVirtualTable(parent, SWT.BORDER | SWT.NO_FOCUS, context);
+               // CmsUtils.style(table, ArgeoOrgStyle.browserColumn.style());
+               table.filterList("*");
+               table.setLayoutData(new GridData(SWT.LEFT, SWT.FILL, false, true));
+               browserCols.put(context, table);
+               parent.layout(true, true);
+               return table;
+       }
+
+       public void addFilterPanel(Composite parent) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout(new GridLayout(2, false)));
+
+               parentPathTxt = new Text(parent, SWT.NO_FOCUS);
+               parentPathTxt.setEditable(false);
+
+               filterTxt = new Text(parent, SWT.SEARCH | SWT.ICON_CANCEL);
+               filterTxt.setMessage("Filter current list");
+               filterTxt.setLayoutData(EclipseUiUtils.fillWidth());
+               filterTxt.addModifyListener(new ModifyListener() {
+                       private static final long serialVersionUID = 1L;
+
+                       public void modifyText(ModifyEvent event) {
+                               modifyFilter(false);
+                       }
+               });
+               filterTxt.addKeyListener(new KeyListener() {
+                       private static final long serialVersionUID = 2533535233583035527L;
+
+                       @Override
+                       public void keyReleased(KeyEvent e) {
+                       }
+
+                       @Override
+                       public void keyPressed(KeyEvent e) {
+                               boolean shiftPressed = (e.stateMask & SWT.SHIFT) != 0;
+                               // boolean altPressed = (e.stateMask & SWT.ALT) != 0;
+                               FilterEntitiesVirtualTable currTable = null;
+                               if (currEdited != null) {
+                                       FilterEntitiesVirtualTable table = browserCols.get(currEdited);
+                                       if (table != null && !table.isDisposed())
+                                               currTable = table;
+                               }
+
+                               if (e.keyCode == SWT.ARROW_DOWN)
+                                       currTable.setFocus();
+                               else if (e.keyCode == SWT.BS) {
+                                       if (filterTxt.getText().equals("")
+                                                       && !(currEdited.getNameCount() == 1 || currEdited.equals(initialPath))) {
+                                               Path oldEdited = currEdited;
+                                               Path parentPath = currEdited.getParent();
+                                               setEdited(parentPath);
+                                               if (browserCols.containsKey(parentPath))
+                                                       browserCols.get(parentPath).setSelected(oldEdited);
+                                               filterTxt.setFocus();
+                                               e.doit = false;
+                                       }
+                               } else if (e.keyCode == SWT.TAB && !shiftPressed) {
+                                       Path uniqueChild = getOnlyChild(currEdited, filterTxt.getText());
+                                       if (uniqueChild != null) {
+                                               // Highlight the unique chosen child
+                                               currTable.setSelected(uniqueChild);
+                                               setEdited(uniqueChild);
+                                       }
+                                       filterTxt.setFocus();
+                                       e.doit = false;
+                               }
+                       }
+               });
+       }
+
+       private Path getOnlyChild(Path parent, String filter) {
+               try (DirectoryStream<Path> stream = Files.newDirectoryStream(currEdited, filter + "*")) {
+                       Path uniqueChild = null;
+                       boolean moreThanOne = false;
+                       loop: for (Path entry : stream) {
+                               if (uniqueChild == null) {
+                                       uniqueChild = entry;
+                               } else {
+                                       moreThanOne = true;
+                                       break loop;
+                               }
+                       }
+                       if (!moreThanOne)
+                               return uniqueChild;
+                       return null;
+               } catch (IOException ioe) {
+                       throw new FsUiException(
+                                       "Unable to determine unique child existence and get it under " + parent + " with filter " + filter,
+                                       ioe);
+               }
+       }
+
+       private void setEdited(Path path) {
+               currEdited = path;
+               EclipseUiUtils.clear(displayBoxCmp);
+               populateCurrEditedDisplay(displayBoxCmp, currEdited);
+               refreshFilters(path);
+               refreshBrowser(path);
+       }
+
+       private void refreshFilters(Path path) {
+               parentPathTxt.setText(path.toUri().toString());
+               filterTxt.setText("");
+               filterTxt.getParent().layout();
+       }
+
+       private void refreshBrowser(Path currPath) {
+               Path currParPath = currPath.getParent();
+               Object[][] colMatrix = new Object[browserCols.size()][2];
+
+               int i = 0, currPathIndex = -1, lastLeftOpenedIndex = -1;
+               for (Path path : browserCols.keySet()) {
+                       colMatrix[i][0] = path;
+                       colMatrix[i][1] = browserCols.get(path);
+                       if (currPathIndex >= 0 && lastLeftOpenedIndex < 0 && currParPath != null) {
+                               boolean leaveOpened = path.startsWith(currPath);
+                               if (!leaveOpened)
+                                       lastLeftOpenedIndex = i;
+                       }
+                       if (currParPath.equals(path))
+                               currPathIndex = i;
+                       i++;
+               }
+
+               if (currPathIndex >= 0 && lastLeftOpenedIndex >= 0) {
+                       // dispose and remove useless cols
+                       for (int l = i - 1; l >= lastLeftOpenedIndex; l--) {
+                               ((FilterEntitiesVirtualTable) colMatrix[l][1]).dispose();
+                               browserCols.remove(colMatrix[l][0]);
+                       }
+               }
+
+               if (browserCols.containsKey(currPath)) {
+                       FilterEntitiesVirtualTable currCol = browserCols.get(currPath);
+                       if (currCol.isDisposed()) {
+                               // Does it still happen ?
+                               log.warn(currPath + " browser column was disposed and still listed");
+                               browserCols.remove(currPath);
+                       }
+               }
+
+               if (!browserCols.containsKey(currPath) && Files.isDirectory(currPath))
+                       createBrowserColumn(scrolledCmpBody, currPath);
+
+               scrolledCmpBody.setLayout(EclipseUiUtils.noSpaceGridLayout(new GridLayout(browserCols.size(), false)));
+               scrolledCmpBody.layout(true, true);
+               // also resize the scrolled composite
+               scrolledCmp.layout();
+       }
+
+       private void modifyFilter(boolean fromOutside) {
+               if (!fromOutside)
+                       if (currEdited != null) {
+                               String filter = filterTxt.getText() + "*";
+                               FilterEntitiesVirtualTable table = browserCols.get(currEdited);
+                               if (table != null && !table.isDisposed())
+                                       table.filterList(filter);
+                       }
+       }
+
+       /**
+        * Recreates the content of the box that displays information about the current
+        * selected node.
+        */
+       private void populateCurrEditedDisplay(Composite parent, Path context) {
+               parent.setLayout(new GridLayout());
+
+               // if (isImg(context)) {
+               // EditableImage image = new Img(parent, RIGHT, context, imageWidth);
+               // image.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false,
+               // 2, 1));
+               // }
+
+               try {
+                       Label contextL = new Label(parent, SWT.NONE);
+                       contextL.setText(context.getFileName().toString());
+                       contextL.setFont(EclipseUiUtils.getBoldFont(parent));
+                       addProperty(parent, "Last modified", Files.getLastModifiedTime(context).toString());
+                       addProperty(parent, "Owner", Files.getOwner(context).getName());
+                       if (Files.isDirectory(context)) {
+                               addProperty(parent, "Type", "Folder");
+                       } else {
+                               String mimeType = Files.probeContentType(context);
+                               if (EclipseUiUtils.isEmpty(mimeType))
+                                       mimeType = "<i>Unknown</i>";
+                               addProperty(parent, "Type", mimeType);
+                               addProperty(parent, "Size", FsUiUtils.humanReadableByteCount(Files.size(context), false));
+                       }
+                       parent.layout(true, true);
+               } catch (IOException e) {
+                       throw new FsUiException("Cannot display details for " + context, e);
+               }
+       }
+
+       private void addProperty(Composite parent, String propName, String value) {
+               Label contextL = new Label(parent, SWT.NONE);
+               contextL.setText(propName + ": " + value);
+       }
+
+       /**
+        * Almost canonical implementation of a table that displays the content of a
+        * directory
+        */
+       private class FilterEntitiesVirtualTable extends Composite {
+               private static final long serialVersionUID = 2223410043691844875L;
+
+               // Context
+               private Path context;
+               private Path currSelected = null;
+
+               // UI Objects
+               private FsTableViewer viewer;
+
+               @Override
+               public boolean setFocus() {
+                       if (viewer.getTable().isDisposed())
+                               return false;
+                       if (currSelected != null)
+                               viewer.setSelection(new StructuredSelection(currSelected), true);
+                       else if (viewer.getSelection().isEmpty()) {
+                               Object first = viewer.getElementAt(0);
+                               if (first != null)
+                                       viewer.setSelection(new StructuredSelection(first), true);
+                       }
+                       return viewer.getTable().setFocus();
+               }
+
+               /**
+                * Enable highlighting the correct element in the table when externally browsing
+                * (typically via the command-line-like Text field)
+                */
+               void setSelected(Path selected) {
+                       // to prevent change selection event to be thrown
+                       currSelected = selected;
+                       viewer.setSelection(new StructuredSelection(currSelected), true);
+               }
+
+               void filterList(String filter) {
+                       viewer.setInput(context, filter);
+               }
+
+               public FilterEntitiesVirtualTable(Composite parent, int style, Path context) {
+                       super(parent, SWT.NO_FOCUS);
+                       this.context = context;
+                       createTableViewer(this);
+               }
+
+               private void createTableViewer(final Composite parent) {
+                       parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+                       // We must limit the size of the table otherwise the full list is
+                       // loaded before the layout happens
+                       // Composite listCmp = new Composite(parent, SWT.NO_FOCUS);
+                       // GridData gd = new GridData(SWT.LEFT, SWT.FILL, false, true);
+                       // gd.widthHint = COLUMN_WIDTH;
+                       // listCmp.setLayoutData(gd);
+                       // listCmp.setLayout(EclipseUiUtils.noSpaceGridLayout());
+                       // viewer = new TableViewer(listCmp, SWT.VIRTUAL | SWT.MULTI |
+                       // SWT.V_SCROLL);
+                       // Table table = viewer.getTable();
+                       // table.setLayoutData(EclipseUiUtils.fillAll());
+
+                       viewer = new FsTableViewer(parent, SWT.MULTI);
+                       Table table = viewer.configureDefaultSingleColumnTable(COLUMN_WIDTH);
+
+                       viewer.addSelectionChangedListener(new ISelectionChangedListener() {
+
+                               @Override
+                               public void selectionChanged(SelectionChangedEvent event) {
+                                       IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
+                                       if (selection.isEmpty())
+                                               return;
+                                       Object obj = selection.getFirstElement();
+                                       Path newSelected;
+                                       if (obj instanceof Path)
+                                               newSelected = (Path) obj;
+                                       else if (obj instanceof ParentDir)
+                                               newSelected = ((ParentDir) obj).getPath();
+                                       else
+                                               return;
+                                       if (newSelected.equals(currSelected))
+                                               return;
+                                       currSelected = newSelected;
+                                       setEdited(newSelected);
+
+                               }
+                       });
+
+                       table.addKeyListener(new KeyListener() {
+                               private static final long serialVersionUID = -8083424284436715709L;
+
+                               @Override
+                               public void keyReleased(KeyEvent e) {
+                               }
+
+                               @Override
+                               public void keyPressed(KeyEvent e) {
+                                       IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
+                                       Path selected = null;
+                                       if (!selection.isEmpty())
+                                               selected = ((Path) selection.getFirstElement());
+                                       if (e.keyCode == SWT.ARROW_RIGHT) {
+                                               if (!Files.isDirectory(selected))
+                                                       return;
+                                               if (selected != null) {
+                                                       setEdited(selected);
+                                                       browserCols.get(selected).setFocus();
+                                               }
+                                       } else if (e.keyCode == SWT.ARROW_LEFT) {
+                                               if (context.equals(initialPath))
+                                                       return;
+                                               Path parent = context.getParent();
+                                               if (parent == null)
+                                                       return;
+
+                                               setEdited(parent);
+                                               browserCols.get(parent).setFocus();
+                                       }
+                               }
+                       });
+               }
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FileIconNameLabelProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FileIconNameLabelProvider.java
new file mode 100644 (file)
index 0000000..d3fc1c9
--- /dev/null
@@ -0,0 +1,84 @@
+package org.argeo.eclipse.ui.fs;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/** Basic label provider with icon for NIO file viewers */
+public class FileIconNameLabelProvider extends ColumnLabelProvider {
+       private static final long serialVersionUID = 8187902187946523148L;
+
+       private Image folderIcon;
+       private Image fileIcon;
+
+       public FileIconNameLabelProvider() {
+               // if (!PlatformUI.isWorkbenchRunning()) {
+               folderIcon = ImageDescriptor.createFromFile(getClass(), "folder.png").createImage();
+               fileIcon = ImageDescriptor.createFromFile(getClass(), "file.png").createImage();
+               // }
+       }
+
+       @Override
+       public void dispose() {
+               if (folderIcon != null)
+                       folderIcon.dispose();
+               if (fileIcon != null)
+                       fileIcon.dispose();
+               super.dispose();
+       }
+
+       @Override
+       public String getText(Object element) {
+               if (element instanceof Path) {
+                       Path curr = ((Path) element);
+                       Path name = curr.getFileName();
+                       if (name == null)
+                               return "[No name]";
+                       else
+                               return name.toString();
+               } else if (element instanceof ParentDir) {
+                       return "..";
+               }
+               return null;
+       }
+
+       @Override
+       public Image getImage(Object element) {
+               if (element instanceof Path) {
+                       Path curr = ((Path) element);
+                       if (Files.isDirectory(curr))
+                               // if (folderIcon != null)
+                               return folderIcon;
+                       // else
+                       // return
+                       // PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FOLDER);
+                       // else if (fileIcon != null)
+                       return fileIcon;
+                       // else
+                       // return
+                       // PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FILE);
+               } else if (element instanceof ParentDir) {
+                       return folderIcon;
+               }
+               return null;
+       }
+
+       @Override
+       public String getToolTipText(Object element) {
+               if (element instanceof Path) {
+                       Path curr = ((Path) element);
+                       Path name = curr.getFileName();
+                       if (name == null)
+                               return "[No name]";
+                       else
+                               return name.toAbsolutePath().toString();
+               } else if (element instanceof ParentDir) {
+                       return ((ParentDir) element).getPath().toAbsolutePath().toString();
+               }
+               return null;
+       }
+
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsTableViewer.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsTableViewer.java
new file mode 100644 (file)
index 0000000..3476739
--- /dev/null
@@ -0,0 +1,130 @@
+package org.argeo.eclipse.ui.fs;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.eclipse.jface.viewers.CellLabelProvider;
+import org.eclipse.jface.viewers.ILazyContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+
+/**
+ * Canonical implementation of a JFace table viewer to display the content of a
+ * file folder
+ */
+public class FsTableViewer extends TableViewer {
+       private static final long serialVersionUID = -5632407542678477234L;
+
+       private boolean showHiddenItems = false;
+       private boolean folderFirst = true;
+       private boolean reverseOrder = false;
+       private String orderProperty = FsUiConstants.PROPERTY_NAME;
+
+       public FsTableViewer(Composite parent, int style) {
+               super(parent, style | SWT.VIRTUAL);
+       }
+
+       public Table configureDefaultSingleColumnTable(int tableWidthHint) {
+
+               return configureDefaultSingleColumnTable(tableWidthHint, new FileIconNameLabelProvider());
+       }
+
+       public Table configureDefaultSingleColumnTable(int tableWidthHint, CellLabelProvider labelProvider) {
+               Table table = this.getTable();
+               table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               table.setLinesVisible(false);
+               table.setHeaderVisible(false);
+               // CmsUtils.markup(table);
+               // CmsUtils.style(table, MaintenanceStyles.BROWSER_COLUMN);
+
+               TableViewerColumn column = new TableViewerColumn(this, SWT.NONE);
+               TableColumn tcol = column.getColumn();
+               tcol.setWidth(tableWidthHint);
+               column.setLabelProvider(labelProvider);
+               this.setContentProvider(new MyLazyCP());
+               return table;
+       }
+
+       public Table configureDefaultTable(List<ColumnDefinition> columns) {
+               this.setContentProvider(new MyLazyCP());
+               Table table = this.getTable();
+               table.setLinesVisible(true);
+               table.setHeaderVisible(true);
+               // CmsUtils.markup(table);
+               // CmsUtils.style(table, MaintenanceStyles.BROWSER_COLUMN);
+               for (ColumnDefinition colDef : columns) {
+                       TableViewerColumn column = new TableViewerColumn(this, SWT.NONE);
+                       column.setLabelProvider(colDef.getLabelProvider());
+                       TableColumn tcol = column.getColumn();
+                       tcol.setResizable(true);
+                       tcol.setText(colDef.getLabel());
+                       tcol.setWidth(colDef.getMinWidth());
+               }
+               return table;
+       }
+
+       public void setInput(Path dir, String filter) {
+               Path[] rows = FsUiUtils.getChildren(dir, filter, showHiddenItems, folderFirst, orderProperty, reverseOrder);
+               if (rows == null) {
+                       this.setInput(null);
+                       this.setItemCount(0);
+                       return;
+               }
+               boolean isRoot;
+               try {
+                       isRoot = dir.getRoot().equals(dir);
+               } catch (Exception e) {
+                       // FIXME Workaround for JCR root node access
+                       isRoot = dir.toString().equals("/");
+               }
+               final Object[] res;
+               if (isRoot)
+                       res = rows;
+               else {
+                       res = new Object[rows.length + 1];
+                       res[0] = new ParentDir(dir.getParent());
+                       for (int i = 1; i < res.length; i++) {
+                               res[i] = rows[i - 1];
+                       }
+               }
+               this.setInput(res);
+               int length = res.length;
+               this.setItemCount(length);
+               this.refresh();
+       }
+
+       /** Directly displays bookmarks **/
+       public void setPathsInput(Path... paths) {
+               this.setInput((Object[]) paths);
+               this.setItemCount(paths.length);
+               this.refresh();
+       }
+
+       private class MyLazyCP implements ILazyContentProvider {
+               private static final long serialVersionUID = 9096550041395433128L;
+               private Object[] elements;
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+                       // IMPORTANT: don't forget this: an exception will be thrown if
+                       // a selected object is not part of the results anymore.
+                       viewer.setSelection(null);
+                       this.elements = (Object[]) newInput;
+               }
+
+               public void updateElement(int index) {
+                       if (index < elements.length)
+                               FsTableViewer.this.replace(elements[index], index);
+               }
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsTreeViewer.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsTreeViewer.java
new file mode 100644 (file)
index 0000000..f55ead7
--- /dev/null
@@ -0,0 +1,144 @@
+package org.argeo.eclipse.ui.fs;
+
+import java.io.IOException;
+import java.nio.file.DirectoryIteratorException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.TreeViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+/**
+ * Canonical implementation of a JFace TreeViewer to display the content of a
+ * repository
+ */
+public class FsTreeViewer extends TreeViewer {
+       private static final long serialVersionUID = -5632407542678477234L;
+
+       private boolean showHiddenItems = false;
+       private boolean showDirectoryFirst = true;
+       private String orderingProperty = FsUiConstants.PROPERTY_NAME;
+
+       public FsTreeViewer(Composite parent, int style) {
+               super(parent, style | SWT.VIRTUAL);
+       }
+
+       public Tree configureDefaultSingleColumnTable(int tableWidthHint) {
+               Tree tree = this.getTree();
+               tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false));
+               tree.setLinesVisible(true);
+               tree.setHeaderVisible(false);
+//             CmsUtils.markup(tree);
+
+               TreeViewerColumn column = new TreeViewerColumn(this, SWT.NONE);
+               TreeColumn tcol = column.getColumn();
+               tcol.setWidth(tableWidthHint);
+               column.setLabelProvider(new FileIconNameLabelProvider());
+
+               this.setContentProvider(new MyCP());
+               return tree;
+       }
+
+       public Tree configureDefaultTable(List<ColumnDefinition> columns) {
+               this.setContentProvider(new MyCP());
+               Tree tree = this.getTree();
+               tree.setLinesVisible(true);
+               tree.setHeaderVisible(true);
+//             CmsUtils.markup(tree);
+//             CmsUtils.style(tree, MaintenanceStyles.BROWSER_COLUMN);
+               for (ColumnDefinition colDef : columns) {
+                       TreeViewerColumn column = new TreeViewerColumn(this, SWT.NONE);
+                       column.setLabelProvider(colDef.getLabelProvider());
+                       TreeColumn tcol = column.getColumn();
+                       tcol.setResizable(true);
+                       tcol.setText(colDef.getLabel());
+                       tcol.setWidth(colDef.getMinWidth());
+               }
+               return tree;
+       }
+
+       public void setInput(Path dir, String filter) {
+               try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) {
+                       // TODO make this lazy
+                       List<Path> paths = new ArrayList<>();
+                       for (Path entry : stream) {
+                               paths.add(entry);
+                       }
+                       Object[] rows = paths.toArray(new Object[0]);
+                       this.setInput(rows);
+                       // this.setItemCount(rows.length);
+                       this.refresh();
+               } catch (IOException | DirectoryIteratorException e) {
+                       throw new FsUiException("Unable to filter " + dir + " children with filter " + filter, e);
+               }
+       }
+
+       /** Directly displays bookmarks **/
+       public void setPathsInput(Path... paths) {
+               this.setInput((Object[]) paths);
+               // this.setItemCount(paths.length);
+               this.refresh();
+       }
+
+       private class MyCP implements ITreeContentProvider {
+               private static final long serialVersionUID = 9096550041395433128L;
+               private Object[] elements;
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+                       // IMPORTANT: don't forget this: an exception will be thrown if
+                       // a selected object is not part of the results anymore.
+                       viewer.setSelection(null);
+                       this.elements = (Object[]) newInput;
+               }
+
+               @Override
+               public Object[] getElements(Object inputElement) {
+                       return elements;
+               }
+
+               @Override
+               public Object[] getChildren(Object parentElement) {
+                       Path path = (Path) parentElement;
+                       if (!Files.isDirectory(path))
+                               return null;
+                       else
+                               return FsUiUtils.getChildren(path, "*", showHiddenItems, showDirectoryFirst, orderingProperty, false);
+               }
+
+               @Override
+               public Object getParent(Object element) {
+                       Path path = (Path) element;
+                       return path.getParent();
+               }
+
+               @Override
+               public boolean hasChildren(Object element) {
+                       Path path = (Path) element;
+                       try {
+                               if (!Files.isDirectory(path))
+                                       return false;
+                               else
+                                       try (DirectoryStream<Path> children = Files.newDirectoryStream(path, "*")) {
+                                               return children.iterator().hasNext();
+                                       }
+                       } catch (IOException e) {
+                               throw new FsUiException("Unable to check child existence on " + path, e);
+                       }
+               }
+
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsUiConstants.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsUiConstants.java
new file mode 100644 (file)
index 0000000..2b51e71
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.eclipse.ui.fs;
+
+/** Centralize constants used by the Nio FS UI parts */
+public interface FsUiConstants {
+
+       // TODO use standard properties
+       String PROPERTY_NAME = "name";
+       String PROPERTY_SIZE = "size";
+       String PROPERTY_LAST_MODIFIED = "last-modified";
+       String PROPERTY_TYPE = "type";
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsUiException.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsUiException.java
new file mode 100644 (file)
index 0000000..422b0e1
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.eclipse.ui.fs;
+
+/** Files specific exception */
+public class FsUiException extends RuntimeException {
+       private static final long serialVersionUID = 1L;
+
+       public FsUiException(String message) {
+               super(message);
+       }
+
+       public FsUiException(String message, Throwable e) {
+               super(message, e);
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsUiUtils.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/FsUiUtils.java
new file mode 100644 (file)
index 0000000..956d96b
--- /dev/null
@@ -0,0 +1,132 @@
+package org.argeo.eclipse.ui.fs;
+
+import java.io.IOException;
+import java.nio.file.DirectoryIteratorException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Centralise additional utilitary methods to manage Java7 NIO files */
+public class FsUiUtils {
+
+       /**
+        * thanks to
+        * http://programming.guide/java/formatting-byte-size-to-human-readable-format.html
+        */
+       public static String humanReadableByteCount(long bytes, boolean si) {
+               int unit = si ? 1000 : 1024;
+               if (bytes < unit)
+                       return bytes + " B";
+               int exp = (int) (Math.log(bytes) / Math.log(unit));
+               String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
+               return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
+       }
+
+       public static Path[] getChildren(Path parent, String filter, boolean showHiddenItems, boolean folderFirst,
+                       String orderProperty, boolean reverseOrder) {
+               if (!Files.isDirectory(parent))
+                       return null;
+               List<Pair> pairs = new ArrayList<>();
+               try (DirectoryStream<Path> stream = Files.newDirectoryStream(parent, filter)) {
+                       loop: for (Path entry : stream) {
+                               if (!showHiddenItems)
+                                       if (Files.isHidden(entry))
+                                               continue loop;
+                               switch (orderProperty) {
+                               case FsUiConstants.PROPERTY_SIZE:
+                                       if (folderFirst)
+                                               pairs.add(new LPair(entry, Files.size(entry), Files.isDirectory(entry)));
+                                       else
+                                               pairs.add(new LPair(entry, Files.size(entry)));
+                                       break;
+                               case FsUiConstants.PROPERTY_LAST_MODIFIED:
+                                       if (folderFirst)
+                                               pairs.add(new LPair(entry, Files.getLastModifiedTime(entry).toMillis(),
+                                                               Files.isDirectory(entry)));
+                                       else
+                                               pairs.add(new LPair(entry, Files.getLastModifiedTime(entry).toMillis()));
+                                       break;
+                               case FsUiConstants.PROPERTY_NAME:
+                                       if (folderFirst)
+                                               pairs.add(new SPair(entry, entry.getFileName().toString(), Files.isDirectory(entry)));
+                                       else
+                                               pairs.add(new SPair(entry, entry.getFileName().toString()));
+                                       break;
+                               default:
+                                       throw new FsUiException("Unable to prepare sort for property " + orderProperty);
+                               }
+                       }
+                       Pair[] rows = pairs.toArray(new Pair[0]);
+                       Arrays.sort(rows);
+                       Path[] results = new Path[rows.length];
+                       if (reverseOrder) {
+                               int j = rows.length - 1;
+                               for (int i = 0; i < rows.length; i++)
+                                       results[i] = rows[j - i].p;
+                       } else
+                               for (int i = 0; i < rows.length; i++)
+                                       results[i] = rows[i].p;
+                       return results;
+               } catch (IOException | DirectoryIteratorException e) {
+                       throw new FsUiException("Unable to filter " + parent + " children with filter " + filter, e);
+               }
+       }
+
+       static abstract class Pair implements Comparable<Object> {
+               Path p;
+               Boolean i;
+       };
+
+       static class LPair extends Pair {
+               long v;
+
+               public LPair(Path path, long propValue) {
+                       p = path;
+                       v = propValue;
+               }
+
+               public LPair(Path path, long propValue, boolean isDir) {
+                       p = path;
+                       v = propValue;
+                       i = isDir;
+               }
+
+               public int compareTo(Object o) {
+                       if (i != null) {
+                               Boolean j = ((LPair) o).i;
+                               if (i.booleanValue() != j.booleanValue())
+                                       return i.booleanValue() ? -1 : 1;
+                       }
+                       long u = ((LPair) o).v;
+                       return v < u ? -1 : v == u ? 0 : 1;
+               }
+       };
+
+       static class SPair extends Pair {
+               String v;
+
+               public SPair(Path path, String propValue) {
+                       p = path;
+                       v = propValue;
+               }
+
+               public SPair(Path path, String propValue, boolean isDir) {
+                       p = path;
+                       v = propValue;
+                       i = isDir;
+               }
+
+               public int compareTo(Object o) {
+                       if (i != null) {
+                               Boolean j = ((SPair) o).i;
+                               if (i.booleanValue() != j.booleanValue())
+                                       return i.booleanValue() ? -1 : 1;
+                       }
+                       String u = ((SPair) o).v;
+                       return v.compareTo(u);
+               }
+       };
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/NioFileLabelProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/NioFileLabelProvider.java
new file mode 100644 (file)
index 0000000..d8cb1d8
--- /dev/null
@@ -0,0 +1,60 @@
+package org.argeo.eclipse.ui.fs;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+
+/** Expect a {@link Path} as input element */
+public class NioFileLabelProvider extends ColumnLabelProvider {
+       private static final long serialVersionUID = 2160026425187796930L;
+       private final String propName;
+
+       public NioFileLabelProvider(String propName) {
+               this.propName = propName;
+       }
+
+       @Override
+       public String getText(Object element) {
+               try {
+                       if (element instanceof ParentDir) {
+                               switch (propName) {
+                               case FsUiConstants.PROPERTY_SIZE:
+                                       return "-";
+                               case FsUiConstants.PROPERTY_LAST_MODIFIED:
+                                       return "-";
+                               // return Files.getLastModifiedTime(((ParentDir) element).getPath()).toString();
+                               case FsUiConstants.PROPERTY_TYPE:
+                                       return "Folder";
+                               }
+                       }
+
+                       Path path = (Path) element;
+                       switch (propName) {
+                       case FsUiConstants.PROPERTY_SIZE:
+                               if (Files.isDirectory(path))
+                                       return "-";
+                               else
+                                       return FsUiUtils.humanReadableByteCount(Files.size(path), false);
+                       case FsUiConstants.PROPERTY_LAST_MODIFIED:
+                               return Files.getLastModifiedTime(path).toString();
+                       case FsUiConstants.PROPERTY_TYPE:
+                               if (Files.isDirectory(path))
+                                       return "Folder";
+                               else {
+                                       String mimeType = Files.probeContentType(path);
+                                       if (EclipseUiUtils.isEmpty(mimeType))
+                                               return "Unknown";
+                                       else
+                                               return mimeType;
+                               }
+                       default:
+                               throw new IllegalArgumentException("Unsupported property " + propName);
+                       }
+               } catch (IOException ioe) {
+                       throw new FsUiException("Cannot get property " + propName + " on " + element);
+               }
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/ParentDir.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/ParentDir.java
new file mode 100644 (file)
index 0000000..6f09c29
--- /dev/null
@@ -0,0 +1,28 @@
+package org.argeo.eclipse.ui.fs;
+
+import java.nio.file.Path;
+
+/** A parent directory (..) reference. */
+public class ParentDir {
+       Path path;
+
+       public ParentDir(Path path) {
+               super();
+               this.path = path;
+       }
+
+       public Path getPath() {
+               return path;
+       }
+
+       @Override
+       public int hashCode() {
+               return path.hashCode();
+       }
+
+       @Override
+       public String toString() {
+               return "Parent dir " + path;
+       }
+
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/SimpleFsBrowser.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/SimpleFsBrowser.java
new file mode 100644 (file)
index 0000000..ff93d82
--- /dev/null
@@ -0,0 +1,212 @@
+package org.argeo.eclipse.ui.fs;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+
+/**
+ * Experimental UI upon Java 7 nio files api: SashForm layout with bookmarks on
+ * the left hand side and a simple table on the right hand side.
+ */
+public class SimpleFsBrowser extends Composite {
+       private final static Log log = LogFactory.getLog(SimpleFsBrowser.class);
+       private static final long serialVersionUID = -40347919096946585L;
+
+       private Path currSelected;
+       private FsTableViewer bookmarksViewer;
+       private FsTableViewer directoryDisplayViewer;
+
+       public SimpleFsBrowser(Composite parent, int style) {
+               super(parent, style);
+               createContent(this);
+               // parent.layout(true, true);
+       }
+
+       public Viewer getViewer() {
+               return directoryDisplayViewer;
+       }
+
+       private void createContent(Composite parent) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+
+               SashForm form = new SashForm(parent, SWT.HORIZONTAL);
+               Composite leftCmp = new Composite(form, SWT.NONE);
+               populateBookmarks(leftCmp);
+
+               Composite rightCmp = new Composite(form, SWT.BORDER);
+               populateDisplay(rightCmp);
+               form.setLayoutData(EclipseUiUtils.fillAll());
+               form.setWeights(new int[] { 1, 3 });
+       }
+
+       public void setInput(Path... paths) {
+               bookmarksViewer.setPathsInput(paths);
+               bookmarksViewer.getTable().getParent().layout(true, true);
+       }
+
+       private void populateBookmarks(final Composite parent) {
+               // GridLayout layout = EclipseUiUtils.noSpaceGridLayout();
+               // layout.verticalSpacing = 5;
+               parent.setLayout(new GridLayout());
+
+               ISelectionChangedListener selList = new MySelectionChangedListener();
+
+               appendTitle(parent, "My bookmarks");
+               bookmarksViewer = new FsTableViewer(parent, SWT.MULTI | SWT.NO_SCROLL);
+               Table table = bookmarksViewer.configureDefaultSingleColumnTable(500);
+               GridData gd = EclipseUiUtils.fillWidth();
+               gd.horizontalIndent = 10;
+               table.setLayoutData(gd);
+               bookmarksViewer.addSelectionChangedListener(selList);
+
+               appendTitle(parent, "Jcr + File");
+
+               FsTableViewer jcrFilesViewers = new FsTableViewer(parent, SWT.MULTI | SWT.NO_SCROLL);
+               table = jcrFilesViewers.configureDefaultSingleColumnTable(500);
+               gd = EclipseUiUtils.fillWidth();
+               gd.horizontalIndent = 10;
+               table.setLayoutData(gd);
+               jcrFilesViewers.addSelectionChangedListener(selList);
+
+               // FileSystemProvider fsProvider = new JackrabbitMemoryFsProvider();
+               // try {
+               // Path testPath = fsProvider.getPath(new URI("jcr+memory:/"));
+               // jcrFilesViewers.setPathsInput(testPath);
+               // } catch (URISyntaxException e) {
+               // // TODO Auto-generated catch block
+               // e.printStackTrace();
+               // }
+       }
+
+       private Label appendTitle(Composite parent, String value) {
+               Label titleLbl = new Label(parent, SWT.NONE);
+               titleLbl.setText(value);
+               titleLbl.setFont(EclipseUiUtils.getBoldFont(parent));
+               GridData gd = EclipseUiUtils.fillWidth();
+               gd.horizontalIndent = 5;
+               gd.verticalIndent = 5;
+               titleLbl.setLayoutData(gd);
+               return titleLbl;
+       }
+
+       private class MySelectionChangedListener implements ISelectionChangedListener {
+               @Override
+               public void selectionChanged(SelectionChangedEvent event) {
+                       IStructuredSelection selection = (IStructuredSelection) bookmarksViewer.getSelection();
+                       if (selection.isEmpty())
+                               return;
+                       else {
+                               Path newSelected = (Path) selection.getFirstElement();
+                               if (newSelected.equals(currSelected))
+                                       return;
+                               currSelected = newSelected;
+                               directoryDisplayViewer.setInput(currSelected, "*");
+                       }
+               }
+       }
+
+       private void populateDisplay(final Composite parent) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+               directoryDisplayViewer = new FsTableViewer(parent, SWT.MULTI);
+               List<ColumnDefinition> colDefs = new ArrayList<>();
+               colDefs.add(new ColumnDefinition(new FileIconNameLabelProvider(), "Name", 200));
+               colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_SIZE), "Size", 100));
+               colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_TYPE), "Type", 250));
+               colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_LAST_MODIFIED),
+                               "Last modified", 200));
+               Table table = directoryDisplayViewer.configureDefaultTable(colDefs);
+               table.setLayoutData(EclipseUiUtils.fillAll());
+
+               table.addKeyListener(new KeyListener() {
+                       private static final long serialVersionUID = -8083424284436715709L;
+
+                       @Override
+                       public void keyReleased(KeyEvent e) {
+                       }
+
+                       @Override
+                       public void keyPressed(KeyEvent e) {
+                               log.debug("Key event received: " + e.keyCode);
+                               IStructuredSelection selection = (IStructuredSelection) directoryDisplayViewer.getSelection();
+                               Path selected = null;
+                               if (!selection.isEmpty())
+                                       selected = ((Path) selection.getFirstElement());
+                               if (e.keyCode == SWT.CR) {
+                                       if (!Files.isDirectory(selected))
+                                               return;
+                                       if (selected != null) {
+                                               currSelected = selected;
+                                               directoryDisplayViewer.setInput(currSelected, "*");
+                                       }
+                               } else if (e.keyCode == SWT.BS) {
+                                       currSelected = currSelected.getParent();
+                                       directoryDisplayViewer.setInput(currSelected, "*");
+                                       directoryDisplayViewer.getTable().setFocus();
+                               }
+                       }
+               });
+
+//             directoryDisplayViewer.addDoubleClickListener(new IDoubleClickListener() {
+//                     @Override
+//                     public void doubleClick(DoubleClickEvent event) {
+//                             IStructuredSelection selection = (IStructuredSelection) directoryDisplayViewer.getSelection();
+//                             Path selected = null;
+//                             if (!selection.isEmpty()) {
+//                                     Object obj = selection.getFirstElement();
+//                                     if (obj instanceof Path)
+//                                             selected = (Path) obj;
+//                                     else if (obj instanceof ParentDir)
+//                                             selected = ((ParentDir) obj).getPath();
+//                             }
+//                             if (selected != null) {
+//                                     if (!Files.isDirectory(selected))
+//                                             return;
+//                                     currSelected = selected;
+//                                     directoryDisplayViewer.setInput(currSelected, "*");
+//                             }
+//                     }
+//             });
+
+               directoryDisplayViewer.addDoubleClickListener(new IDoubleClickListener() {
+                       @Override
+                       public void doubleClick(DoubleClickEvent event) {
+                               IStructuredSelection selection = (IStructuredSelection) directoryDisplayViewer.getSelection();
+                               Path selected = null;
+                               if (!selection.isEmpty()) {
+                                       Object obj = selection.getFirstElement();
+                                       if (obj instanceof Path)
+                                               selected = (Path) obj;
+                                       else if (obj instanceof ParentDir)
+                                               selected = ((ParentDir) obj).getPath();
+                               }
+                               if (selected != null) {
+                                       if (!Files.isDirectory(selected))
+                                               return;
+                                       currSelected = selected;
+                                       directoryDisplayViewer.setInput(currSelected, "*");
+                               }
+                       }
+               });
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/SimpleFsTreeBrowser.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/SimpleFsTreeBrowser.java
new file mode 100644 (file)
index 0000000..f8128d9
--- /dev/null
@@ -0,0 +1,129 @@
+package org.argeo.eclipse.ui.fs;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.Tree;
+
+/** A simple Java 7 nio files browser with a tree */
+public class SimpleFsTreeBrowser extends Composite {
+       private final static Log log = LogFactory.getLog(SimpleFsTreeBrowser.class);
+       private static final long serialVersionUID = -40347919096946585L;
+
+       private Path currSelected;
+       private FsTreeViewer treeViewer;
+       private FsTableViewer directoryDisplayViewer;
+
+       public SimpleFsTreeBrowser(Composite parent, int style) {
+               super(parent, style);
+               createContent(this);
+               // parent.layout(true, true);
+       }
+
+       private void createContent(Composite parent) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+               SashForm form = new SashForm(parent, SWT.HORIZONTAL);
+               Composite child1 = new Composite(form, SWT.NONE);
+               populateTree(child1);
+               Composite child2 = new Composite(form, SWT.BORDER);
+               populateDisplay(child2);
+               form.setLayoutData(EclipseUiUtils.fillAll());
+               form.setWeights(new int[] { 1, 3 });
+       }
+
+       public void setInput(Path... paths) {
+               treeViewer.setPathsInput(paths);
+               treeViewer.getControl().getParent().layout(true, true);
+       }
+
+       private void populateTree(final Composite parent) {
+               // GridLayout layout = EclipseUiUtils.noSpaceGridLayout();
+               // layout.verticalSpacing = 5;
+               parent.setLayout(new GridLayout());
+
+               ISelectionChangedListener selList = new MySelectionChangedListener();
+
+               treeViewer = new FsTreeViewer(parent, SWT.MULTI);
+               Tree tree = treeViewer.configureDefaultSingleColumnTable(500);
+               GridData gd = EclipseUiUtils.fillAll();
+               // gd.horizontalIndent = 10;
+               tree.setLayoutData(gd);
+               treeViewer.addSelectionChangedListener(selList);
+       }
+
+       private class MySelectionChangedListener implements ISelectionChangedListener {
+               @Override
+               public void selectionChanged(SelectionChangedEvent event) {
+                       IStructuredSelection selection = (IStructuredSelection) treeViewer.getSelection();
+                       if (selection.isEmpty())
+                               return;
+                       else {
+                               Path newSelected = (Path) selection.getFirstElement();
+                               if (newSelected.equals(currSelected))
+                                       return;
+                               currSelected = newSelected;
+                               if (Files.isDirectory(currSelected))
+                                       directoryDisplayViewer.setInput(currSelected, "*");
+                       }
+               }
+       }
+
+       private void populateDisplay(final Composite parent) {
+               parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+               directoryDisplayViewer = new FsTableViewer(parent, SWT.MULTI);
+               List<ColumnDefinition> colDefs = new ArrayList<>();
+               colDefs.add(new ColumnDefinition(new FileIconNameLabelProvider(), "Name", 200, 200));
+               colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_SIZE), "Size", 100, 100));
+               colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_TYPE), "Type", 300, 300));
+               colDefs.add(new ColumnDefinition(new NioFileLabelProvider(FsUiConstants.PROPERTY_LAST_MODIFIED),
+                               "Last modified", 100, 100));
+               Table table = directoryDisplayViewer.configureDefaultTable(colDefs);
+               table.setLayoutData(EclipseUiUtils.fillAll());
+
+               table.addKeyListener(new KeyListener() {
+                       private static final long serialVersionUID = -8083424284436715709L;
+
+                       @Override
+                       public void keyReleased(KeyEvent e) {
+                       }
+
+                       @Override
+                       public void keyPressed(KeyEvent e) {
+                               log.debug("Key event received: " + e.keyCode);
+                               IStructuredSelection selection = (IStructuredSelection) directoryDisplayViewer.getSelection();
+                               Path selected = null;
+                               if (!selection.isEmpty())
+                                       selected = ((Path) selection.getFirstElement());
+                               if (e.keyCode == SWT.CR) {
+                                       if (!Files.isDirectory(selected))
+                                               return;
+                                       if (selected != null) {
+                                               currSelected = selected;
+                                               directoryDisplayViewer.setInput(currSelected, "*");
+                                       }
+                               } else if (e.keyCode == SWT.BS) {
+                                       currSelected = currSelected.getParent();
+                                       directoryDisplayViewer.setInput(currSelected, "*");
+                                       directoryDisplayViewer.getTable().setFocus();
+                               }
+                       }
+               });
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/file.png b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/file.png
new file mode 100644 (file)
index 0000000..b168263
Binary files /dev/null and b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/file.png differ
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/folder.png b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/folder.png
new file mode 100644 (file)
index 0000000..56487e0
Binary files /dev/null and b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/fs/folder.png differ
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/AbstractNodeContentProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/AbstractNodeContentProvider.java
new file mode 100644 (file)
index 0000000..8d80660
--- /dev/null
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.AbstractTreeContentProvider;
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/** Canonical implementation of tree content provider manipulating JCR nodes. */
+public abstract class AbstractNodeContentProvider extends
+               AbstractTreeContentProvider {
+       private static final long serialVersionUID = -4905836490027272569L;
+
+       private final static Log log = LogFactory
+                       .getLog(AbstractNodeContentProvider.class);
+
+       private Session session;
+
+       public AbstractNodeContentProvider(Session session) {
+               this.session = session;
+       }
+
+       /**
+        * Whether this path is a base path (and thus has no parent). By default it
+        * returns true if path is '/' (root node)
+        */
+       protected Boolean isBasePath(String path) {
+               // root node
+               return path.equals("/");
+       }
+
+       @Override
+       public Object[] getChildren(Object element) {
+               Object[] children;
+               if (element instanceof Node) {
+                       try {
+                               Node node = (Node) element;
+                               children = getChildren(node);
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Cannot get children of " + element, e);
+                       }
+               } else if (element instanceof WrappedNode) {
+                       WrappedNode wrappedNode = (WrappedNode) element;
+                       try {
+                               children = getChildren(wrappedNode.getNode());
+                       } catch (RepositoryException e) {
+                               throw new EclipseUiException("Cannot get children of "
+                                               + wrappedNode, e);
+                       }
+               } else if (element instanceof NodesWrapper) {
+                       NodesWrapper node = (NodesWrapper) element;
+                       children = node.getChildren();
+               } else {
+                       children = super.getChildren(element);
+               }
+
+               children = sort(element, children);
+               return children;
+       }
+
+       /** Do not sort by default. To be overidden to provide custom sort. */
+       protected Object[] sort(Object parent, Object[] children) {
+               return children;
+       }
+
+       /**
+        * To be overridden in order to filter out some nodes. Does nothing by
+        * default. The provided list is a temporary one and can thus be modified
+        * directly . (e.g. via an iterator)
+        */
+       protected List<Node> filterChildren(List<Node> children)
+                       throws RepositoryException {
+               return children;
+       }
+
+       protected Object[] getChildren(Node node) throws RepositoryException {
+               List<Node> nodes = new ArrayList<Node>();
+               for (NodeIterator nit = node.getNodes(); nit.hasNext();)
+                       nodes.add(nit.nextNode());
+               nodes = filterChildren(nodes);
+               return nodes.toArray();
+       }
+
+       @Override
+       public Object getParent(Object element) {
+               if (element instanceof Node) {
+                       Node node = (Node) element;
+                       try {
+                               String path = node.getPath();
+                               if (isBasePath(path))
+                                       return null;
+                               else
+                                       return node.getParent();
+                       } catch (RepositoryException e) {
+                               log.warn("Cannot get parent of " + element + ": " + e);
+                               return null;
+                       }
+               } else if (element instanceof WrappedNode) {
+                       WrappedNode wrappedNode = (WrappedNode) element;
+                       return wrappedNode.getParent();
+               } else if (element instanceof NodesWrapper) {
+                       NodesWrapper nodesWrapper = (NodesWrapper) element;
+                       return this.getParent(nodesWrapper.getNode());
+               }
+               return super.getParent(element);
+       }
+
+       @Override
+       public boolean hasChildren(Object element) {
+               try {
+                       if (element instanceof Node) {
+                               Node node = (Node) element;
+                               return node.hasNodes();
+                       } else if (element instanceof WrappedNode) {
+                               WrappedNode wrappedNode = (WrappedNode) element;
+                               return wrappedNode.getNode().hasNodes();
+                       } else if (element instanceof NodesWrapper) {
+                               NodesWrapper nodesWrapper = (NodesWrapper) element;
+                               return nodesWrapper.hasChildren();
+                       }
+
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot check whether " + element
+                                       + " has children", e);
+               }
+               return super.hasChildren(element);
+       }
+
+       public Session getSession() {
+               return session;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java
new file mode 100644 (file)
index 0000000..88119b8
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * {@link EventListener} which simplifies running actions within the UI thread.
+ */
+public abstract class AsyncUiEventListener implements EventListener {
+       // private final static Log logSuper = LogFactory
+       // .getLog(AsyncUiEventListener.class);
+       private final Log logThis = LogFactory.getLog(getClass());
+
+       private final Display display;
+
+       public AsyncUiEventListener(Display display) {
+               super();
+               this.display = display;
+       }
+
+       /** Called asynchronously in the UI thread. */
+       protected abstract void onEventInUiThread(List<Event> events) throws RepositoryException;
+
+       /**
+        * Whether these events should be processed in the UI or skipped with no UI
+        * job created.
+        */
+       protected Boolean willProcessInUiThread(List<Event> events) throws RepositoryException {
+               return true;
+       }
+
+       protected Log getLog() {
+               return logThis;
+       }
+
+       public final void onEvent(final EventIterator eventIterator) {
+               final List<Event> events = new ArrayList<Event>();
+               while (eventIterator.hasNext())
+                       events.add(eventIterator.nextEvent());
+
+               if (logThis.isTraceEnabled())
+                       logThis.trace("Received " + events.size() + " events");
+
+               try {
+                       if (!willProcessInUiThread(events))
+                               return;
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot test skip events " + events, e);
+               }
+
+               // Job job = new Job("JCR Events") {
+               // protected IStatus run(IProgressMonitor monitor) {
+               // if (display.isDisposed()) {
+               // logSuper.warn("Display is disposed cannot update UI");
+               // return Status.CANCEL_STATUS;
+               // }
+
+               if (!display.isDisposed())
+                       display.asyncExec(new Runnable() {
+                               public void run() {
+                                       try {
+                                               onEventInUiThread(events);
+                                       } catch (RepositoryException e) {
+                                               throw new EclipseUiException("Cannot process events " + events, e);
+                                       }
+                               }
+                       });
+
+               // return Status.OK_STATUS;
+               // }
+               // };
+               // job.schedule();
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/DefaultNodeLabelProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/DefaultNodeLabelProvider.java
new file mode 100644 (file)
index 0000000..e8157b2
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Default label provider to manage node and corresponding UI objects. It
+ * provides reasonable overwrite-able default for known JCR types.
+ */
+public class DefaultNodeLabelProvider extends ColumnLabelProvider {
+       private static final long serialVersionUID = 1216182332792151235L;
+
+       public String getText(Object element) {
+               try {
+                       if (element instanceof Node) {
+                               return getText((Node) element);
+                       } else if (element instanceof WrappedNode) {
+                               return getText(((WrappedNode) element).getNode());
+                       } else if (element instanceof NodesWrapper) {
+                               return getText(((NodesWrapper) element).getNode());
+                       }
+                       return super.getText(element);
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot get text for of " + element, e);
+               }
+       }
+
+       protected String getText(Node node) throws RepositoryException {
+               if (node.isNodeType(NodeType.MIX_TITLE)
+                               && node.hasProperty(Property.JCR_TITLE))
+                       return node.getProperty(Property.JCR_TITLE).getString();
+               else
+                       return node.getName();
+       }
+
+       @Override
+       public Image getImage(Object element) {
+               try {
+                       if (element instanceof Node) {
+                               return getImage((Node) element);
+                       } else if (element instanceof WrappedNode) {
+                               return getImage(((WrappedNode) element).getNode());
+                       } else if (element instanceof NodesWrapper) {
+                               return getImage(((NodesWrapper) element).getNode());
+                       }
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot retrieve image for " + element, e);
+               }
+               return super.getImage(element);
+       }
+
+       protected Image getImage(Node node) throws RepositoryException {
+               // FIXME who uses that?
+               return null;
+       }
+
+       @Override
+       public String getToolTipText(Object element) {
+               try {
+                       if (element instanceof Node) {
+                               return getToolTipText((Node) element);
+                       } else if (element instanceof WrappedNode) {
+                               return getToolTipText(((WrappedNode) element).getNode());
+                       } else if (element instanceof NodesWrapper) {
+                               return getToolTipText(((NodesWrapper) element).getNode());
+                       }
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot get tooltip for " + element, e);
+               }
+               return super.getToolTipText(element);
+       }
+
+       protected String getToolTipText(Node node) throws RepositoryException {
+               return null;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/JcrUiUtils.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/JcrUiUtils.java
new file mode 100644 (file)
index 0000000..420154b
--- /dev/null
@@ -0,0 +1,149 @@
+package org.argeo.eclipse.ui.jcr;
+
+import java.util.Calendar;
+
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.jcr.lists.NodeViewerComparator;
+import org.argeo.eclipse.ui.jcr.lists.RowViewerComparator;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.widgets.Table;
+
+/** Utility methods to simplify UI development using SWT (or RWT), jface  and JCR. */
+public class JcrUiUtils {
+
+       /**
+        * Centralizes management of updating property value. Among other to avoid
+        * infinite loop when the new value is the same as the ones that is already
+        * stored in JCR.
+        * 
+        * @return true if the value as changed
+        */
+       public static boolean setJcrProperty(Node node, String propName,
+                       int propertyType, Object value) {
+               try {
+                       switch (propertyType) {
+                       case PropertyType.STRING:
+                               if ("".equals((String) value)
+                                               && (!node.hasProperty(propName) || node
+                                                               .hasProperty(propName)
+                                                               && "".equals(node.getProperty(propName)
+                                                                               .getString())))
+                                       // workaround the fact that the Text widget value cannot be
+                                       // set to null
+                                       return false;
+                               else if (node.hasProperty(propName)
+                                               && node.getProperty(propName).getString()
+                                                               .equals((String) value))
+                                       // nothing changed yet
+                                       return false;
+                               else {
+                                       node.setProperty(propName, (String) value);
+                                       return true;
+                               }
+                       case PropertyType.BOOLEAN:
+                               if (node.hasProperty(propName)
+                                               && node.getProperty(propName).getBoolean() == (Boolean) value)
+                                       // nothing changed yet
+                                       return false;
+                               else {
+                                       node.setProperty(propName, (Boolean) value);
+                                       return true;
+                               }
+                       case PropertyType.DATE:
+                               if (node.hasProperty(propName)
+                                               && node.getProperty(propName).getDate()
+                                                               .equals((Calendar) value))
+                                       // nothing changed yet
+                                       return false;
+                               else {
+                                       node.setProperty(propName, (Calendar) value);
+                                       return true;
+                               }
+                       case PropertyType.LONG:
+                               Long lgValue = (Long) value;
+
+                               if (lgValue == null)
+                                       lgValue = 0L;
+
+                               if (node.hasProperty(propName)
+                                               && node.getProperty(propName).getLong() == lgValue)
+                                       // nothing changed yet
+                                       return false;
+                               else {
+                                       node.setProperty(propName, lgValue);
+                                       return true;
+                               }
+
+                       default:
+                               throw new EclipseUiException("Unimplemented property save");
+                       }
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Unexpected error while setting property",
+                                       re);
+               }
+       }
+
+       /**
+        * Creates a new selection adapter in order to provide sorting abitily on a
+        * SWT Table that display a row list
+        **/
+       public static SelectionAdapter getRowSelectionAdapter(final int index,
+                       final int propertyType, final String selectorName,
+                       final String propertyName, final RowViewerComparator comparator,
+                       final TableViewer viewer) {
+               SelectionAdapter selectionAdapter = new SelectionAdapter() {
+                       private static final long serialVersionUID = -5738918304901437720L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               Table table = viewer.getTable();
+                               comparator.setColumn(propertyType, selectorName, propertyName);
+                               int dir = table.getSortDirection();
+                               if (table.getSortColumn() == table.getColumn(index)) {
+                                       dir = dir == SWT.UP ? SWT.DOWN : SWT.UP;
+                               } else {
+                                       dir = SWT.DOWN;
+                               }
+                               table.setSortDirection(dir);
+                               table.setSortColumn(table.getColumn(index));
+                               viewer.refresh();
+                       }
+               };
+               return selectionAdapter;
+       }
+
+       /**
+        * Creates a new selection adapter in order to provide sorting abitily on a
+        * swt table that display a row list
+        **/
+       public static SelectionAdapter getNodeSelectionAdapter(final int index,
+                       final int propertyType, final String propertyName,
+                       final NodeViewerComparator comparator, final TableViewer viewer) {
+               SelectionAdapter selectionAdapter = new SelectionAdapter() {
+                       private static final long serialVersionUID = -1683220869195484625L;
+
+                       @Override
+                       public void widgetSelected(SelectionEvent e) {
+                               Table table = viewer.getTable();
+                               comparator.setColumn(propertyType, propertyName);
+                               int dir = table.getSortDirection();
+                               if (table.getSortColumn() == table.getColumn(index)) {
+                                       dir = dir == SWT.UP ? SWT.DOWN : SWT.UP;
+                               } else {
+                                       dir = SWT.DOWN;
+                               }
+                               table.setSortDirection(dir);
+                               table.setSortColumn(table.getColumn(index));
+                               viewer.refresh();
+                       }
+               };
+               return selectionAdapter;
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/NodeElementComparer.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/NodeElementComparer.java
new file mode 100644 (file)
index 0000000..9afb92d
--- /dev/null
@@ -0,0 +1,51 @@
+/*\r
+ * Copyright (C) 2007-2012 Argeo GmbH\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *         http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package org.argeo.eclipse.ui.jcr;\r
+\r
+import javax.jcr.Node;\r
+import javax.jcr.RepositoryException;\r
+\r
+import org.argeo.eclipse.ui.EclipseUiException;\r
+import org.eclipse.jface.viewers.IElementComparer;\r
+\r
+/** Element comparer for JCR node, to be used in JFace viewers. */\r
+public class NodeElementComparer implements IElementComparer {\r
+\r
+       public boolean equals(Object a, Object b) {\r
+               try {\r
+                       if ((a instanceof Node) && (b instanceof Node)) {\r
+                               Node nodeA = (Node) a;\r
+                               Node nodeB = (Node) b;\r
+                               return nodeA.getIdentifier().equals(nodeB.getIdentifier());\r
+                       } else {\r
+                               return a.equals(b);\r
+                       }\r
+               } catch (RepositoryException e) {\r
+                       throw new EclipseUiException("Cannot compare nodes", e);\r
+               }\r
+       }\r
+\r
+       public int hashCode(Object element) {\r
+               try {\r
+                       if (element instanceof Node)\r
+                               return ((Node) element).getIdentifier().hashCode();\r
+                       return element.hashCode();\r
+               } catch (RepositoryException e) {\r
+                       throw new EclipseUiException("Cannot get hash code", e);\r
+               }\r
+       }\r
+\r
+}\r
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/NodesWrapper.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/NodesWrapper.java
new file mode 100644 (file)
index 0000000..d899b00
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/**
+ * Element of tree which is based on a node, but whose children are not
+ * necessarily this node children.
+ */
+public class NodesWrapper {
+       private final Node node;
+
+       public NodesWrapper(Node node) {
+               super();
+               this.node = node;
+       }
+
+       protected NodeIterator getNodeIterator() throws RepositoryException {
+               return node.getNodes();
+       }
+
+       protected List<WrappedNode> getWrappedNodes() throws RepositoryException {
+               List<WrappedNode> nodes = new ArrayList<WrappedNode>();
+               for (NodeIterator nit = getNodeIterator(); nit.hasNext();)
+                       nodes.add(new WrappedNode(this, nit.nextNode()));
+               return nodes;
+       }
+
+       public Object[] getChildren() {
+               try {
+                       return getWrappedNodes().toArray();
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot get wrapped children", e);
+               }
+       }
+
+       /**
+        * @return true by default because we don't want to compute the wrapped
+        *         nodes twice
+        */
+       public Boolean hasChildren() {
+               return true;
+       }
+
+       public Node getNode() {
+               return node;
+       }
+
+       @Override
+       public int hashCode() {
+               return node.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof NodesWrapper)
+                       return node.equals(((NodesWrapper) obj).getNode());
+               else
+                       return false;
+       }
+
+       public String toString() {
+               return "nodes wrapper based on " + node;
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/SimpleNodeContentProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/SimpleNodeContentProvider.java
new file mode 100644 (file)
index 0000000..43848f6
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.jcr;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.jcr.JcrUtils;
+
+/** Simple JCR node content provider taking a list of String as base path. */
+public class SimpleNodeContentProvider extends AbstractNodeContentProvider {
+       private static final long serialVersionUID = -8245193308831384269L;
+       private final List<String> basePaths;
+       private Boolean mkdirs = false;
+
+       public SimpleNodeContentProvider(Session session, String... basePaths) {
+               this(session, Arrays.asList(basePaths));
+       }
+
+       public SimpleNodeContentProvider(Session session, List<String> basePaths) {
+               super(session);
+               this.basePaths = basePaths;
+       }
+
+       @Override
+       protected Boolean isBasePath(String path) {
+               if (basePaths.contains(path))
+                       return true;
+               return super.isBasePath(path);
+       }
+
+       public Object[] getElements(Object inputElement) {
+               try {
+                       List<Node> baseNodes = new ArrayList<Node>();
+                       for (String basePath : basePaths)
+                               if (mkdirs && !getSession().itemExists(basePath))
+                                       baseNodes.add(JcrUtils.mkdirs(getSession(), basePath));
+                               else
+                                       baseNodes.add(getSession().getNode(basePath));
+                       return baseNodes.toArray();
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot get base nodes for " + basePaths,
+                                       e);
+               }
+       }
+
+       public List<String> getBasePaths() {
+               return basePaths;
+       }
+
+       public void setMkdirs(Boolean mkdirs) {
+               this.mkdirs = mkdirs;
+       }
+
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/WrappedNode.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/WrappedNode.java
new file mode 100644 (file)
index 0000000..91dab99
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.jcr;
+
+import javax.jcr.Node;
+
+/** Wrap a node (created from a {@link NodesWrapper}) */
+public class WrappedNode {
+       private final NodesWrapper parent;
+       private final Node node;
+
+       public WrappedNode(NodesWrapper parent, Node node) {
+               super();
+               this.parent = parent;
+               this.node = node;
+       }
+
+       public NodesWrapper getParent() {
+               return parent;
+       }
+
+       public Node getNode() {
+               return node;
+       }
+
+       public String toString() {
+               return "wrapped " + node;
+       }
+
+       @Override
+       public int hashCode() {
+               return node.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof WrappedNode)
+                       return node.equals(((WrappedNode) obj).getNode());
+               else
+                       return false;
+       }
+
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/JcrColumnDefinition.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/JcrColumnDefinition.java
new file mode 100644 (file)
index 0000000..c5dd733
--- /dev/null
@@ -0,0 +1,116 @@
+package org.argeo.eclipse.ui.jcr.lists;
+
+import javax.jcr.Node;
+import javax.jcr.query.Row;
+
+import org.argeo.eclipse.ui.ColumnDefinition;
+
+/**
+ * Utility object to manage column in various tables and extracts displaying
+ * data from JCR
+ */
+public class JcrColumnDefinition extends ColumnDefinition {
+       private final static int DEFAULT_COLUMN_SIZE = 120;
+
+       private String selectorName;
+       private String propertyName;
+       private int propertyType;
+       private int columnSize;
+
+       /**
+        * Use this kind of columns to configure a table that displays JCR
+        * {@link Row}
+        * 
+        * @param selectorName
+        * @param propertyName
+        * @param propertyType
+        * @param headerLabel
+        */
+       public JcrColumnDefinition(String selectorName, String propertyName,
+                       int propertyType, String headerLabel) {
+               super(new SimpleJcrRowLabelProvider(selectorName, propertyName),
+                               headerLabel);
+               this.selectorName = selectorName;
+               this.propertyName = propertyName;
+               this.propertyType = propertyType;
+               this.columnSize = DEFAULT_COLUMN_SIZE;
+       }
+
+       /**
+        * Use this kind of columns to configure a table that displays JCR
+        * {@link Row}
+        * 
+        * @param selectorName
+        * @param propertyName
+        * @param propertyType
+        * @param headerLabel
+        * @param columnSize
+        */
+       public JcrColumnDefinition(String selectorName, String propertyName,
+                       int propertyType, String headerLabel, int columnSize) {
+               super(new SimpleJcrRowLabelProvider(selectorName, propertyName),
+                               headerLabel, columnSize);
+               this.selectorName = selectorName;
+               this.propertyName = propertyName;
+               this.propertyType = propertyType;
+               this.columnSize = columnSize;
+       }
+
+       /**
+        * Use this kind of columns to configure a table that displays JCR
+        * {@link Node}
+        * 
+        * @param propertyName
+        * @param propertyType
+        * @param headerLabel
+        * @param columnSize
+        */
+       public JcrColumnDefinition(String propertyName, int propertyType,
+                       String headerLabel, int columnSize) {
+               super(new SimpleJcrNodeLabelProvider(propertyName), headerLabel,
+                               columnSize);
+               this.propertyName = propertyName;
+               this.propertyType = propertyType;
+               this.columnSize = columnSize;
+       }
+
+       public String getSelectorName() {
+               return selectorName;
+       }
+
+       public void setSelectorName(String selectorName) {
+               this.selectorName = selectorName;
+       }
+
+       public String getPropertyName() {
+               return propertyName;
+       }
+
+       public void setPropertyName(String propertyName) {
+               this.propertyName = propertyName;
+       }
+
+       public int getPropertyType() {
+               return propertyType;
+       }
+
+       public void setPropertyType(int propertyType) {
+               this.propertyType = propertyType;
+       }
+
+       public int getColumnSize() {
+               return columnSize;
+       }
+
+       public void setColumnSize(int columnSize) {
+               this.columnSize = columnSize;
+       }
+
+       public String getHeaderLabel() {
+               return super.getLabel();
+       }
+
+       public void setHeaderLabel(String headerLabel) {
+               super.setLabel(headerLabel);
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/NodeViewerComparator.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/NodeViewerComparator.java
new file mode 100644 (file)
index 0000000..341b3ab
--- /dev/null
@@ -0,0 +1,190 @@
+package org.argeo.eclipse.ui.jcr.lists;
+
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerComparator;
+
+/**
+ * Base comparator to enable ordering on Table or Tree viewer that display Jcr
+ * Nodes.
+ * 
+ * Note that the following snippet must be added before setting the comparator
+ * to the corresponding control: <code>
+ * // IMPORTANT: initialize comparator before setting it
+ * JcrColumnDefinition firstCol = colDefs.get(0);
+ * comparator.setColumn(firstCol.getPropertyType(),
+ * firstCol.getPropertyName());
+ * viewer.setComparator(comparator); </code>
+ */
+public class NodeViewerComparator extends ViewerComparator {
+       private static final long serialVersionUID = -7782916140737279027L;
+
+       protected String propertyName;
+
+       protected int propertyType;
+       public static final int ASCENDING = 0, DESCENDING = 1;
+       protected int direction = DESCENDING;
+
+       public NodeViewerComparator() {
+       }
+
+       /**
+        * e1 and e2 must both be Jcr nodes.
+        * 
+        * @param viewer
+        * @param e1
+        * @param e2
+        * @return
+        */
+       @Override
+       public int compare(Viewer viewer, Object e1, Object e2) {
+               int rc = 0;
+               long lc = 0;
+
+               try {
+                       Node n1 = (Node) e1;
+                       Node n2 = (Node) e2;
+
+                       Value v1 = null;
+                       Value v2 = null;
+                       if (n1.hasProperty(propertyName))
+                               v1 = n1.getProperty(propertyName).getValue();
+                       if (n2.hasProperty(propertyName))
+                               v2 = n2.getProperty(propertyName).getValue();
+
+                       if (v2 == null && v1 == null)
+                               return 0;
+                       else if (v2 == null)
+                               return -1;
+                       else if (v1 == null)
+                               return 1;
+
+                       switch (propertyType) {
+                       case PropertyType.STRING:
+                               rc = v1.getString().compareTo(v2.getString());
+                               break;
+                       case PropertyType.BOOLEAN:
+                               boolean b1 = v1.getBoolean();
+                               boolean b2 = v2.getBoolean();
+                               if (b1 == b2)
+                                       rc = 0;
+                               else
+                                       // we assume true is greater than false
+                                       rc = b1 ? 1 : -1;
+                               break;
+                       case PropertyType.DATE:
+                               Calendar c1 = v1.getDate();
+                               Calendar c2 = v2.getDate();
+                               if (c1 == null || c2 == null)
+                                       // log.trace("undefined date");
+                                       ;
+                               lc = c1.getTimeInMillis() - c2.getTimeInMillis();
+                               if (lc < Integer.MIN_VALUE)
+                                       rc = -1;
+                               else if (lc > Integer.MAX_VALUE)
+                                       rc = 1;
+                               else
+                                       rc = (int) lc;
+                               break;
+                       case PropertyType.LONG:
+                               long l1;
+                               long l2;
+                               // TODO Sometimes an empty string is set instead of a long
+                               try {
+                                       l1 = v1.getLong();
+                               } catch (ValueFormatException ve) {
+                                       l1 = 0;
+                               }
+                               try {
+                                       l2 = v2.getLong();
+                               } catch (ValueFormatException ve) {
+                                       l2 = 0;
+                               }
+
+                               lc = l1 - l2;
+                               if (lc < Integer.MIN_VALUE)
+                                       rc = -1;
+                               else if (lc > Integer.MAX_VALUE)
+                                       rc = 1;
+                               else
+                                       rc = (int) lc;
+                               break;
+                       case PropertyType.DECIMAL:
+                               BigDecimal bd1 = v1.getDecimal();
+                               BigDecimal bd2 = v2.getDecimal();
+                               rc = bd1.compareTo(bd2);
+                               break;
+                       case PropertyType.DOUBLE:
+                               Double d1 = v1.getDouble();
+                               Double d2 = v2.getDouble();
+                               rc = d1.compareTo(d2);
+                               break;
+                       default:
+                               throw new EclipseUiException(
+                                               "Unimplemented comparaison for PropertyType "
+                                                               + propertyType);
+                       }
+                       // If descending order, flip the direction
+                       if (direction == DESCENDING) {
+                               rc = -rc;
+                       }
+
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Unexpected error "
+                                       + "while comparing nodes", re);
+               }
+               return rc;
+       }
+
+       /**
+        * @param propertyType
+        *            Corresponding JCR type
+        * @param propertyName
+        *            name of the property to use.
+        */
+       public void setColumn(int propertyType, String propertyName) {
+               if (this.propertyName != null && this.propertyName.equals(propertyName)) {
+                       // Same column as last sort; toggle the direction
+                       direction = 1 - direction;
+               } else {
+                       // New column; do an ascending sort
+                       this.propertyType = propertyType;
+                       this.propertyName = propertyName;
+                       direction = ASCENDING;
+               }
+       }
+
+       // Getters and setters
+       protected String getPropertyName() {
+               return propertyName;
+       }
+
+       protected void setPropertyName(String propertyName) {
+               this.propertyName = propertyName;
+       }
+
+       protected int getPropertyType() {
+               return propertyType;
+       }
+
+       protected void setPropertyType(int propertyType) {
+               this.propertyType = propertyType;
+       }
+
+       protected int getDirection() {
+               return direction;
+       }
+
+       protected void setDirection(int direction) {
+               this.direction = direction;
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/RowViewerComparator.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/RowViewerComparator.java
new file mode 100644 (file)
index 0000000..455fb0d
--- /dev/null
@@ -0,0 +1,62 @@
+package org.argeo.eclipse.ui.jcr.lists;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.query.Row;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Base comparator to enable ordering on Table or Tree viewer that display Jcr
+ * rows
+ */
+public class RowViewerComparator extends NodeViewerComparator {
+       private static final long serialVersionUID = 7020939505172625113L;
+       protected String selectorName;
+
+       public RowViewerComparator() {
+       }
+
+       /**
+        * e1 and e2 must both be Jcr rows.
+        * 
+        * @param viewer
+        * @param e1
+        * @param e2
+        * @return
+        */
+       @Override
+       public int compare(Viewer viewer, Object e1, Object e2) {
+               try {
+                       Node n1 = ((Row) e1).getNode(selectorName);
+                       Node n2 = ((Row) e2).getNode(selectorName);
+                       return super.compare(viewer, n1, n2);
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Unexpected error "
+                                       + "while comparing nodes", re);
+               }
+       }
+
+       /**
+        * @param propertyType
+        *            Corresponding JCR type
+        * @param propertyName
+        *            name of the property to use.
+        */
+       public void setColumn(int propertyType, String selectorName,
+                       String propertyName) {
+               if (this.selectorName != null && getPropertyName() != null
+                               && this.selectorName.equals(selectorName)
+                               && this.getPropertyName().equals(propertyName)) {
+                       // Same column as last sort; toggle the direction
+                       setDirection(1 - getDirection());
+               } else {
+                       // New column; do a descending sort
+                       setPropertyType(propertyType);
+                       setPropertyName(propertyName);
+                       this.selectorName = selectorName;
+                       setDirection(NodeViewerComparator.ASCENDING);
+               }
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrNodeLabelProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrNodeLabelProvider.java
new file mode 100644 (file)
index 0000000..aa2e337
--- /dev/null
@@ -0,0 +1,120 @@
+package org.argeo.eclipse.ui.jcr.lists;
+
+import java.text.DateFormat;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.text.SimpleDateFormat;
+
+import javax.jcr.Node;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+
+/** Base implementation of a label provider for controls that display JCR Nodes */
+public class SimpleJcrNodeLabelProvider extends ColumnLabelProvider {
+       private static final long serialVersionUID = -5215787695436221993L;
+
+       private final static String DEFAULT_DATE_FORMAT = "EEE, dd MMM yyyy";
+       private final static String DEFAULT_NUMBER_FORMAT = "#,##0.0";
+
+       private DateFormat dateFormat;
+       private NumberFormat numberFormat;
+
+       final private String propertyName;
+
+       /**
+        * Default Label provider for a given property of a node. Using default
+        * pattern for date and number formating
+        */
+       public SimpleJcrNodeLabelProvider(String propertyName) {
+               this.propertyName = propertyName;
+               dateFormat = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
+               numberFormat = DecimalFormat.getInstance();
+               ((DecimalFormat) numberFormat).applyPattern(DEFAULT_NUMBER_FORMAT);
+       }
+
+       /**
+        * Label provider for a given property of a node optionally precising date
+        * and/or number format patterns
+        */
+       public SimpleJcrNodeLabelProvider(String propertyName,
+                       String dateFormatPattern, String numberFormatPattern) {
+               this.propertyName = propertyName;
+               dateFormat = new SimpleDateFormat(
+                               dateFormatPattern == null ? DEFAULT_DATE_FORMAT
+                                               : dateFormatPattern);
+               numberFormat = DecimalFormat.getInstance();
+               ((DecimalFormat) numberFormat)
+                               .applyPattern(numberFormatPattern == null ? DEFAULT_NUMBER_FORMAT
+                                               : numberFormatPattern);
+       }
+
+       @Override
+       public String getText(Object element) {
+               try {
+                       Node currNode = (Node) element;
+
+                       if (currNode.hasProperty(propertyName)) {
+                               if (currNode.getProperty(propertyName).isMultiple()) {
+                                       StringBuilder builder = new StringBuilder();
+                                       for (Value value : currNode.getProperty(propertyName)
+                                                       .getValues()) {
+                                               String currStr = getSingleValueAsString(value);
+                                               if (notEmptyString(currStr))
+                                                       builder.append(currStr).append("; ");
+                                       }
+                                       if (builder.length() > 0)
+                                               builder.deleteCharAt(builder.length() - 2);
+
+                                       return builder.toString();
+                               } else
+                                       return getSingleValueAsString(currNode.getProperty(
+                                                       propertyName).getValue());
+                       } else
+                               return "";
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Unable to get text from row", re);
+               }
+       }
+
+       private String getSingleValueAsString(Value value)
+                       throws RepositoryException {
+               switch (value.getType()) {
+               case PropertyType.STRING:
+                       return value.getString();
+               case PropertyType.BOOLEAN:
+                       return "" + value.getBoolean();
+               case PropertyType.DATE:
+                       return dateFormat.format(value.getDate().getTime());
+               case PropertyType.LONG:
+                       return "" + value.getLong();
+               case PropertyType.DECIMAL:
+                       return numberFormat.format(value.getDecimal());
+               case PropertyType.DOUBLE:
+                       return numberFormat.format(value.getDouble());
+               case PropertyType.NAME:
+                       return value.getString();
+               default:
+                       throw new EclipseUiException("Unimplemented label provider "
+                                       + "for property type " + value.getType()
+                                       + " while getting property " + propertyName + " - value: "
+                                       + value.getString());
+
+               }
+       }
+
+       private boolean notEmptyString(String string) {
+               return string != null && !"".equals(string.trim());
+       }
+
+       public void setDateFormat(String dateFormatPattern) {
+               dateFormat = new SimpleDateFormat(dateFormatPattern);
+       }
+
+       public void setNumberFormat(String numberFormatPattern) {
+               ((DecimalFormat) numberFormat).applyPattern(numberFormatPattern);
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrRowLabelProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrRowLabelProvider.java
new file mode 100644 (file)
index 0000000..5d421f6
--- /dev/null
@@ -0,0 +1,47 @@
+package org.argeo.eclipse.ui.jcr.lists;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.query.Row;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/**
+ * Base implementation of a label provider for widgets that display JCR Rows.
+ */
+public class SimpleJcrRowLabelProvider extends SimpleJcrNodeLabelProvider {
+       private static final long serialVersionUID = -3414654948197181740L;
+
+       final private String selectorName;
+
+       /**
+        * Default Label provider for a given property of a row. Using default
+        * pattern for date and number formating
+        */
+       public SimpleJcrRowLabelProvider(String selectorName, String propertyName) {
+               super(propertyName);
+               this.selectorName = selectorName;
+       }
+
+       /**
+        * Label provider for a given property of a node optionally precising date
+        * and/or number format patterns
+        */
+       public SimpleJcrRowLabelProvider(String selectorName, String propertyName,
+                       String dateFormatPattern, String numberFormatPattern) {
+               super(propertyName, dateFormatPattern, numberFormatPattern);
+               this.selectorName = selectorName;
+       }
+
+       @Override
+       public String getText(Object element) {
+               try {
+                       Row currRow = (Row) element;
+                       Node currNode = currRow.getNode(selectorName);
+                       return super.getText(currNode);
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Unable to get Node " + selectorName
+                                       + " from row " + element, re);
+               }
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/JcrFileProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/JcrFileProvider.java
new file mode 100644 (file)
index 0000000..472101f
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.jcr.utils;
+
+import java.io.InputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.FileProvider;
+
+/**
+ * Implements a FileProvider for UI purposes. Note that it might not be very
+ * reliable as long as we have not fixed login and multi repository issues that
+ * will be addressed in the next version.
+ * 
+ * NOTE: id used here is the real id of the JCR Node, not the JCR Path
+ * 
+ * Relies on common approach for JCR file handling implementation.
+ * 
+ */
+@SuppressWarnings("deprecation")
+public class JcrFileProvider implements FileProvider {
+
+       // private Object[] rootNodes;
+       private Node refNode;
+
+       /**
+        * Must be set in order for the provider to be able to get current session
+        * and thus have the ability to get the file node corresponding to a given
+        * file ID
+        * 
+        * @param refNode
+        */
+       public void setReferenceNode(Node refNode) {
+               // FIXME : this introduces some concurrency ISSUES.
+               this.refNode = refNode;
+       }
+
+       public byte[] getByteArrayFileFromId(String fileId) {
+               InputStream fis = null;
+               byte[] ba = null;
+               Node child = getFileNodeFromId(fileId);
+               try {
+                       fis = (InputStream) child.getProperty(Property.JCR_DATA).getBinary().getStream();
+                       ba = IOUtils.toByteArray(fis);
+
+               } catch (Exception e) {
+                       throw new EclipseUiException("Stream error while opening file", e);
+               } finally {
+                       IOUtils.closeQuietly(fis);
+               }
+               return ba;
+       }
+
+       public InputStream getInputStreamFromFileId(String fileId) {
+               try {
+                       InputStream fis = null;
+
+                       Node child = getFileNodeFromId(fileId);
+                       fis = (InputStream) child.getProperty(Property.JCR_DATA).getBinary().getStream();
+                       return fis;
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot get stream from file node for Id " + fileId, re);
+               }
+       }
+
+       /**
+        * Throws an exception if the node is not found in the current repository (a
+        * bit like a FileNotFoundException)
+        * 
+        * @param fileId
+        * @return Returns the child node of the nt:file node. It is the child node
+        *         that have the jcr:data property where actual file is stored.
+        *         never null
+        */
+       private Node getFileNodeFromId(String fileId) {
+               try {
+                       Node result = refNode.getSession().getNodeByIdentifier(fileId);
+
+                       // rootNodes: for (int j = 0; j < rootNodes.length; j++) {
+                       // // in case we have a classic JCR Node
+                       // if (rootNodes[j] instanceof Node) {
+                       // Node curNode = (Node) rootNodes[j];
+                       // if (result != null)
+                       // break rootNodes;
+                       // } // Case of a repository Node
+                       // else if (rootNodes[j] instanceof RepositoryNode) {
+                       // Object[] nodes = ((RepositoryNode) rootNodes[j])
+                       // .getChildren();
+                       // for (int i = 0; i < nodes.length; i++) {
+                       // Node node = (Node) nodes[i];
+                       // result = node.getSession().getNodeByIdentifier(fileId);
+                       // if (result != null)
+                       // break rootNodes;
+                       // }
+                       // }
+                       // }
+
+                       // Sanity checks
+                       if (result == null)
+                               throw new EclipseUiException("File node not found for ID" + fileId);
+
+                       Node child = null;
+
+                       boolean isValid = true;
+                       if (!result.isNodeType(NodeType.NT_FILE))
+                               // useless: mandatory child node
+                               // || !result.hasNode(Property.JCR_CONTENT))
+                               isValid = false;
+                       else {
+                               child = result.getNode(Property.JCR_CONTENT);
+                               if (!(child.isNodeType(NodeType.NT_RESOURCE) || child.hasProperty(Property.JCR_DATA)))
+                                       isValid = false;
+                       }
+
+                       if (!isValid)
+                               throw new EclipseUiException("ERROR: In the current implemented model, '" + NodeType.NT_FILE
+                                               + "' file node must have a child node named jcr:content "
+                                               + "that has a BINARY Property named jcr:data " + "where the actual data is stored");
+                       return child;
+
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Erreur while getting file node of ID " + fileId, re);
+               }
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/JcrItemsComparator.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/JcrItemsComparator.java
new file mode 100644 (file)
index 0000000..5f17f41
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.jcr.utils;
+
+import java.util.Comparator;
+
+import javax.jcr.Item;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+
+/** Compares two JCR items (node or properties) based on their names. */
+public class JcrItemsComparator implements Comparator<Item> {
+       public int compare(Item o1, Item o2) {
+               try {
+                       // TODO: put folder before files
+                       return o1.getName().compareTo(o2.getName());
+               } catch (RepositoryException e) {
+                       throw new EclipseUiException("Cannot compare " + o1 + " and " + o2, e);
+               }
+       }
+
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/NodeViewerComparer.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/NodeViewerComparer.java
new file mode 100644 (file)
index 0000000..db8ca08
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.jcr.utils;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.eclipse.jface.viewers.IElementComparer;
+
+/** Compare JCR nodes based on their JCR identifiers, for use in JFace viewers. */
+public class NodeViewerComparer implements IElementComparer {
+
+       // force comparison on Node IDs only.
+       public boolean equals(Object elementA, Object elementB) {
+               if (!(elementA instanceof Node) || !(elementB instanceof Node)) {
+                       return elementA == null ? elementB == null : elementA
+                                       .equals(elementB);
+               } else {
+
+                       boolean result = false;
+                       try {
+                               String idA = ((Node) elementA).getIdentifier();
+                               String idB = ((Node) elementB).getIdentifier();
+                               result = idA == null ? idB == null : idA.equals(idB);
+                       } catch (RepositoryException re) {
+                               throw new EclipseUiException("cannot compare nodes", re);
+                       }
+
+                       return result;
+               }
+       }
+
+       public int hashCode(Object element) {
+               // TODO enhanced this method.
+               return element.getClass().toString().hashCode();
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/SingleSessionFileProvider.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/jcr/utils/SingleSessionFileProvider.java
new file mode 100644 (file)
index 0000000..05754d0
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.jcr.utils;
+
+import java.io.InputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.IOUtils;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.FileProvider;
+
+/**
+ * Implements a FileProvider for UI purposes. Unlike the
+ * <code> JcrFileProvider </code>, it relies on a single session and manages
+ * nodes with path only.
+ * 
+ * Note that considered id is the JCR path
+ * 
+ * Relies on common approach for JCR file handling implementation.
+ */
+@SuppressWarnings("deprecation")
+public class SingleSessionFileProvider implements FileProvider {
+
+       private Session session;
+
+       public SingleSessionFileProvider(Session session) {
+               this.session = session;
+       }
+
+       public byte[] getByteArrayFileFromId(String fileId) {
+               InputStream fis = null;
+               byte[] ba = null;
+               Node child = getFileNodeFromId(fileId);
+               try {
+                       fis = (InputStream) child.getProperty(Property.JCR_DATA)
+                                       .getBinary().getStream();
+                       ba = IOUtils.toByteArray(fis);
+
+               } catch (Exception e) {
+                       throw new EclipseUiException("Stream error while opening file", e);
+               } finally {
+                       IOUtils.closeQuietly(fis);
+               }
+               return ba;
+       }
+
+       public InputStream getInputStreamFromFileId(String fileId) {
+               try {
+                       InputStream fis = null;
+
+                       Node child = getFileNodeFromId(fileId);
+                       fis = (InputStream) child.getProperty(Property.JCR_DATA)
+                                       .getBinary().getStream();
+                       return fis;
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Cannot get stream from file node for Id "
+                                       + fileId, re);
+               }
+       }
+
+       /**
+        * 
+        * @param fileId
+        * @return Returns the child node of the nt:file node. It is the child node
+        *         that have the jcr:data property where actual file is stored.
+        *         never null
+        */
+       private Node getFileNodeFromId(String fileId) {
+               try {
+                       Node result = null;
+                       result = session.getNode(fileId);
+
+                       // Sanity checks
+                       if (result == null)
+                               throw new EclipseUiException("File node not found for ID" + fileId);
+
+                       // Ensure that the node have the correct type.
+                       if (!result.isNodeType(NodeType.NT_FILE))
+                               throw new EclipseUiException(
+                                               "Cannot open file children Node that are not of "
+                                                               + NodeType.NT_RESOURCE + " type.");
+
+                       Node child = result.getNodes().nextNode();
+                       if (child == null || !child.isNodeType(NodeType.NT_RESOURCE))
+                               throw new EclipseUiException(
+                                               "ERROR: IN the current implemented model, "
+                                                               + NodeType.NT_FILE
+                                                               + "  file node must have one and only one child of the nt:ressource, where actual data is stored");
+                       return child;
+               } catch (RepositoryException re) {
+                       throw new EclipseUiException("Erreur while getting file node of ID "
+                                       + fileId, re);
+               }
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/parts/LdifUsersTable.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/parts/LdifUsersTable.java
new file mode 100644 (file)
index 0000000..d6dc82c
--- /dev/null
@@ -0,0 +1,402 @@
+package org.argeo.eclipse.ui.parts;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.argeo.eclipse.ui.ColumnDefinition;
+import org.argeo.eclipse.ui.EclipseUiException;
+import org.argeo.eclipse.ui.EclipseUiUtils;
+import org.argeo.eclipse.ui.utils.ViewerUtils;
+import org.eclipse.jface.layout.TableColumnLayout;
+import org.eclipse.jface.viewers.CheckboxTableViewer;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ColumnWeightData;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Text;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Generic composite that display a filter and a table viewer to display users
+ * (can also be groups)
+ * 
+ * Warning: this class does not extends <code>TableViewer</code>. Use the
+ * getTableViewer method to access it.
+ * 
+ */
+public abstract class LdifUsersTable extends Composite {
+       private static final long serialVersionUID = -7385959046279360420L;
+
+       // Context
+       // private UserAdmin userAdmin;
+
+       // Configuration
+       private List<ColumnDefinition> columnDefs = new ArrayList<ColumnDefinition>();
+       private boolean hasFilter;
+       private boolean preventTableLayout = false;
+       private boolean hasSelectionColumn;
+       private int tableStyle;
+
+       // Local UI Objects
+       private TableViewer usersViewer;
+       private Text filterTxt;
+
+       /* EXPOSED METHODS */
+
+       /**
+        * @param parent
+        * @param style
+        */
+       public LdifUsersTable(Composite parent, int style) {
+               super(parent, SWT.NO_FOCUS);
+               this.tableStyle = style;
+       }
+
+       // TODO workaround the bug of the table layout in the Form
+       public LdifUsersTable(Composite parent, int style, boolean preventTableLayout) {
+               super(parent, SWT.NO_FOCUS);
+               this.tableStyle = style;
+               this.preventTableLayout = preventTableLayout;
+       }
+
+       /** This must be called before the call to populate method */
+       public void setColumnDefinitions(List<ColumnDefinition> columnDefinitions) {
+               this.columnDefs = columnDefinitions;
+       }
+
+       /**
+        * 
+        * @param addFilter
+        *            choose to add a field to filter results or not
+        * @param addSelection
+        *            choose to add a column to select some of the displayed results or
+        *            not
+        */
+       public void populate(boolean addFilter, boolean addSelection) {
+               // initialization
+               Composite parent = this;
+               hasFilter = addFilter;
+               hasSelectionColumn = addSelection;
+
+               // Main Layout
+               GridLayout layout = EclipseUiUtils.noSpaceGridLayout();
+               layout.verticalSpacing = 5;
+               this.setLayout(layout);
+               if (hasFilter)
+                       createFilterPart(parent);
+
+               Composite tableComp = new Composite(parent, SWT.NO_FOCUS);
+               tableComp.setLayoutData(EclipseUiUtils.fillAll());
+               usersViewer = createTableViewer(tableComp);
+               usersViewer.setContentProvider(new UsersContentProvider());
+       }
+
+       /**
+        * 
+        * @param showMore
+        *            display static filters on creation
+        * @param addSelection
+        *            choose to add a column to select some of the displayed results or
+        *            not
+        */
+       public void populateWithStaticFilters(boolean showMore, boolean addSelection) {
+               // initialization
+               Composite parent = this;
+               hasFilter = true;
+               hasSelectionColumn = addSelection;
+
+               // Main Layout
+               GridLayout layout = EclipseUiUtils.noSpaceGridLayout();
+               layout.verticalSpacing = 5;
+               this.setLayout(layout);
+               createStaticFilterPart(parent, showMore);
+
+               Composite tableComp = new Composite(parent, SWT.NO_FOCUS);
+               tableComp.setLayoutData(EclipseUiUtils.fillAll());
+               usersViewer = createTableViewer(tableComp);
+               usersViewer.setContentProvider(new UsersContentProvider());
+       }
+
+       /** Enable access to the selected users or groups */
+       public List<User> getSelectedUsers() {
+               if (hasSelectionColumn) {
+                       Object[] elements = ((CheckboxTableViewer) usersViewer).getCheckedElements();
+
+                       List<User> result = new ArrayList<User>();
+                       for (Object obj : elements) {
+                               result.add((User) obj);
+                       }
+                       return result;
+               } else
+                       throw new EclipseUiException(
+                                       "Unvalid request: no selection column " + "has been created for the current table");
+       }
+
+       /** Returns the User table viewer, typically to add doubleclick listener */
+       public TableViewer getTableViewer() {
+               return usersViewer;
+       }
+
+       /**
+        * Force the refresh of the underlying table using the current filter string if
+        * relevant
+        */
+       public void refresh() {
+               String filter = hasFilter ? filterTxt.getText().trim() : null;
+               if ("".equals(filter))
+                       filter = null;
+               refreshFilteredList(filter);
+       }
+
+       /** Effective repository request: caller must implement this method */
+       abstract protected List<User> listFilteredElements(String filter);
+
+       // protected List<User> listFilteredElements(String filter) {
+       // List<User> users = new ArrayList<User>();
+       // try {
+       // Role[] roles = userAdmin.getRoles(filter);
+       // // Display all users and groups
+       // for (Role role : roles)
+       // users.add((User) role);
+       // } catch (InvalidSyntaxException e) {
+       // throw new EclipseUiException("Unable to get roles with filter: "
+       // + filter, e);
+       // }
+       // return users;
+       // }
+
+       /* GENERIC COMPOSITE METHODS */
+       @Override
+       public boolean setFocus() {
+               if (hasFilter)
+                       return filterTxt.setFocus();
+               else
+                       return usersViewer.getTable().setFocus();
+       }
+
+       @Override
+       public void dispose() {
+               super.dispose();
+       }
+
+       /* LOCAL CLASSES AND METHODS */
+       // Will be usefull to rather use a virtual table viewer
+       private void refreshFilteredList(String filter) {
+               List<User> users = listFilteredElements(filter);
+               usersViewer.setInput(users.toArray());
+       }
+
+       private class UsersContentProvider implements IStructuredContentProvider {
+               private static final long serialVersionUID = 1L;
+
+               public Object[] getElements(Object inputElement) {
+                       return (Object[]) inputElement;
+               }
+
+               public void dispose() {
+               }
+
+               public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+               }
+       }
+
+       /* MANAGE FILTER */
+       private void createFilterPart(Composite parent) {
+               // Text Area for the filter
+               filterTxt = new Text(parent, SWT.BORDER | SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL);
+               filterTxt.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false));
+               filterTxt.addModifyListener(new ModifyListener() {
+                       private static final long serialVersionUID = 1L;
+
+                       public void modifyText(ModifyEvent event) {
+                               refreshFilteredList(filterTxt.getText());
+                       }
+               });
+       }
+
+       private void createStaticFilterPart(Composite parent, boolean showMore) {
+               Composite filterComp = new Composite(parent, SWT.NO_FOCUS);
+               filterComp.setLayout(new GridLayout(2, false));
+               filterComp.setLayoutData(EclipseUiUtils.fillWidth());
+               // generic search
+               filterTxt = new Text(filterComp, SWT.BORDER | SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL);
+               filterTxt.setLayoutData(new GridData(GridData.FILL, GridData.FILL, true, false));
+               // filterTxt.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL |
+               // GridData.HORIZONTAL_ALIGN_FILL));
+               filterTxt.addModifyListener(new ModifyListener() {
+                       private static final long serialVersionUID = 1L;
+
+                       public void modifyText(ModifyEvent event) {
+                               refreshFilteredList(filterTxt.getText());
+                       }
+               });
+
+               // add static filter abilities
+               Link moreLk = new Link(filterComp, SWT.NONE);
+               Composite staticFilterCmp = new Composite(filterComp, SWT.NO_FOCUS);
+               staticFilterCmp.setLayoutData(EclipseUiUtils.fillWidth(2));
+               populateStaticFilters(staticFilterCmp);
+
+               MoreLinkListener listener = new MoreLinkListener(moreLk, staticFilterCmp, showMore);
+               // initialise the layout
+               listener.refresh();
+               moreLk.addSelectionListener(listener);
+       }
+
+       /** Overwrite to add static filters */
+       protected void populateStaticFilters(Composite staticFilterCmp) {
+       }
+
+       // private void addMoreSL(final Link more) {
+       // more.addSelectionListener( }
+
+       private class MoreLinkListener extends SelectionAdapter {
+               private static final long serialVersionUID = -524987616510893463L;
+               private boolean isShown;
+               private final Composite staticFilterCmp;
+               private final Link moreLk;
+
+               public MoreLinkListener(Link moreLk, Composite staticFilterCmp, boolean isShown) {
+                       this.moreLk = moreLk;
+                       this.staticFilterCmp = staticFilterCmp;
+                       this.isShown = isShown;
+               }
+
+               @Override
+               public void widgetSelected(SelectionEvent e) {
+                       isShown = !isShown;
+                       refresh();
+               }
+
+               public void refresh() {
+                       GridData gd = (GridData) staticFilterCmp.getLayoutData();
+                       if (isShown) {
+                               moreLk.setText("<a> Less... </a>");
+                               gd.heightHint = SWT.DEFAULT;
+                       } else {
+                               moreLk.setText("<a> More... </a>");
+                               gd.heightHint = 0;
+                       }
+                       forceLayout();
+               }
+       }
+
+       private void forceLayout() {
+               LdifUsersTable.this.getParent().layout(true, true);
+       }
+
+       private TableViewer createTableViewer(final Composite parent) {
+
+               int style = tableStyle | SWT.H_SCROLL | SWT.V_SCROLL;
+               if (hasSelectionColumn)
+                       style = style | SWT.CHECK;
+               Table table = new Table(parent, style);
+               TableColumnLayout layout = new TableColumnLayout();
+
+               // TODO the table layout does not works with the scrolled form
+
+               if (preventTableLayout) {
+                       parent.setLayout(EclipseUiUtils.noSpaceGridLayout());
+                       table.setLayoutData(EclipseUiUtils.fillAll());
+               } else
+                       parent.setLayout(layout);
+
+               TableViewer viewer;
+               if (hasSelectionColumn)
+                       viewer = new CheckboxTableViewer(table);
+               else
+                       viewer = new TableViewer(table);
+               table.setLinesVisible(true);
+               table.setHeaderVisible(true);
+
+               TableViewerColumn column;
+               // int offset = 0;
+               if (hasSelectionColumn) {
+                       // offset = 1;
+                       column = ViewerUtils.createTableViewerColumn(viewer, "", SWT.NONE, 25);
+                       column.setLabelProvider(new ColumnLabelProvider() {
+                               private static final long serialVersionUID = 1L;
+
+                               @Override
+                               public String getText(Object element) {
+                                       return null;
+                               }
+                       });
+                       layout.setColumnData(column.getColumn(), new ColumnWeightData(25, 25, false));
+
+                       SelectionAdapter selectionAdapter = new SelectionAdapter() {
+                               private static final long serialVersionUID = 1L;
+
+                               boolean allSelected = false;
+
+                               @Override
+                               public void widgetSelected(SelectionEvent e) {
+                                       allSelected = !allSelected;
+                                       ((CheckboxTableViewer) usersViewer).setAllChecked(allSelected);
+                               }
+                       };
+                       column.getColumn().addSelectionListener(selectionAdapter);
+               }
+
+               // NodeViewerComparator comparator = new NodeViewerComparator();
+               // TODO enable the sort by click on the header
+               // int i = offset;
+               for (ColumnDefinition colDef : columnDefs)
+                       createTableColumn(viewer, layout, colDef);
+
+               // column = ViewerUtils.createTableViewerColumn(viewer,
+               // colDef.getHeaderLabel(), SWT.NONE, colDef.getColumnSize());
+               // column.setLabelProvider(new CLProvider(colDef.getPropertyName()));
+               // column.getColumn().addSelectionListener(
+               // JcrUiUtils.getNodeSelectionAdapter(i,
+               // colDef.getPropertyType(), colDef.getPropertyName(),
+               // comparator, viewer));
+               // i++;
+               // }
+
+               // IMPORTANT: initialize comparator before setting it
+               // JcrColumnDefinition firstCol = colDefs.get(0);
+               // comparator.setColumn(firstCol.getPropertyType(),
+               // firstCol.getPropertyName());
+               // viewer.setComparator(comparator);
+
+               return viewer;
+       }
+
+       /** Default creation of a column for a user table */
+       private TableViewerColumn createTableColumn(TableViewer tableViewer, TableColumnLayout layout,
+                       ColumnDefinition columnDef) {
+
+               boolean resizable = true;
+               TableViewerColumn tvc = new TableViewerColumn(tableViewer, SWT.NONE);
+               TableColumn column = tvc.getColumn();
+
+               column.setText(columnDef.getLabel());
+               column.setWidth(columnDef.getMinWidth());
+               column.setResizable(resizable);
+
+               ColumnLabelProvider lp = columnDef.getLabelProvider();
+               // add a reference to the display to enable font management
+               // if (lp instanceof UserAdminAbstractLP)
+               // ((UserAdminAbstractLP) lp).setDisplay(tableViewer.getTable()
+               // .getDisplay());
+               tvc.setLabelProvider(lp);
+
+               layout.setColumnData(column, new ColumnWeightData(columnDef.getWeight(), columnDef.getMinWidth(), resizable));
+
+               return tvc;
+       }
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/utils/SingleSourcingConstants.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/utils/SingleSourcingConstants.java
new file mode 100644 (file)
index 0000000..a45eeda
--- /dev/null
@@ -0,0 +1,17 @@
+package org.argeo.eclipse.ui.utils;
+
+/**
+ * Centralise constants that are used in both RAP and RCP specific code to avoid
+ * duplicated declaration
+ */
+public interface SingleSourcingConstants {
+
+       // Single sourced open file command
+       String OPEN_FILE_CMD_ID = "org.argeo.cms.ui.workbench.openFile";
+       String PARAM_FILE_NAME = "param.fileName";
+       String PARAM_FILE_URI = "param.fileURI";
+
+       String SCHEME_HOST_SEPARATOR = "://";
+       String FILE_SCHEME = "file";
+       String JCR_SCHEME = "jcr";
+}
diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/utils/ViewerUtils.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/utils/ViewerUtils.java
new file mode 100644 (file)
index 0000000..3c029d3
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.utils;
+
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.jface.viewers.TreeViewerColumn;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.TreeColumn;
+
+/**
+ * Centralise useful methods to manage JFace Table, Tree and TreeColumn viewers.
+ */
+public class ViewerUtils {
+
+       /**
+        * Creates a basic column for the given table. For the time being, we do not
+        * support movable columns.
+        */
+       public static TableColumn createColumn(Table parent, String name, int style, int width) {
+               TableColumn result = new TableColumn(parent, style);
+               result.setText(name);
+               result.setWidth(width);
+               result.setResizable(true);
+               return result;
+       }
+
+       /**
+        * Creates a TableViewerColumn for the given viewer. For the time being, we do
+        * not support movable columns.
+        */
+       public static TableViewerColumn createTableViewerColumn(TableViewer parent, String name, int style, int width) {
+               TableViewerColumn tvc = new TableViewerColumn(parent, style);
+               TableColumn column = tvc.getColumn();
+               column.setText(name);
+               column.setWidth(width);
+               column.setResizable(true);
+               return tvc;
+       }
+
+       // public static TableViewerColumn createTableViewerColumn(TableViewer parent,
+       // Localized name, int style, int width) {
+       // return createTableViewerColumn(parent, name.lead(), style, width);
+       // }
+
+       /**
+        * Creates a TreeViewerColumn for the given viewer. For the time being, we do
+        * not support movable columns.
+        */
+       public static TreeViewerColumn createTreeViewerColumn(TreeViewer parent, String name, int style, int width) {
+               TreeViewerColumn tvc = new TreeViewerColumn(parent, style);
+               TreeColumn column = tvc.getColumn();
+               column.setText(name);
+               column.setWidth(width);
+               column.setResizable(true);
+               return tvc;
+       }
+}
diff --git a/org.argeo.enterprise/.classpath b/org.argeo.enterprise/.classpath
new file mode 100644 (file)
index 0000000..4e5da1d
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" path="src" />
+       <classpathentry kind="src" path="ext/test" />
+       <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-1.8" />
+       <classpathentry kind="output" path="bin" />
+</classpath>
diff --git a/org.argeo.enterprise/.gitignore b/org.argeo.enterprise/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.enterprise/.project b/org.argeo.enterprise/.project
new file mode 100644 (file)
index 0000000..5de2f0a
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.enterprise</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+               <nature>org.eclipse.pde.PluginNature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.enterprise/META-INF/.gitignore b/org.argeo.enterprise/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.enterprise/bnd.bnd b/org.argeo.enterprise/bnd.bnd
new file mode 100644 (file)
index 0000000..4b2eb27
--- /dev/null
@@ -0,0 +1,2 @@
+Import-Package:        org.osgi.*;version=0.0.0,\
+*                              
diff --git a/org.argeo.enterprise/build.properties b/org.argeo.enterprise/build.properties
new file mode 100644 (file)
index 0000000..af03ba4
--- /dev/null
@@ -0,0 +1,8 @@
+source.. = src/,\
+           ext/test/
+additional.bundles = org.junit,\
+                     org.slf4j.commons.logging,\
+                     org.slf4j.api,\
+                     org.slf4j.log4j12,\
+                     org.apache.log4j,\
+                     bitronix.tm
diff --git a/org.argeo.enterprise/ext/test/log4j.properties b/org.argeo.enterprise/ext/test/log4j.properties
new file mode 100644 (file)
index 0000000..ef73566
--- /dev/null
@@ -0,0 +1,28 @@
+log4j.rootLogger=WARN, console
+
+## Levels
+log4j.logger.org.argeo=TRACE
+
+log4j.logger.org.hibernate=WARN
+
+log4j.logger.org.springframework=WARN
+#log4j.logger.org.springframework.web=DEBUG
+#log4j.logger.org.springframework.jms=WARN
+#log4j.logger.org.springframework.security=WARN
+
+log4j.logger.org.apache.activemq=WARN
+log4j.logger.org.apache.activemq.transport=WARN
+log4j.logger.org.apache.activemq.ActiveMQMessageConsumer=INFO
+log4j.logger.org.apache.activemq.ActiveMQMessageProducer=INFO
+
+log4j.logger.org.apache.catalina=INFO
+log4j.logger.org.apache.coyote=INFO
+log4j.logger.org.apache.tomcat=INFO
+
+## Appenders
+# console is set to be a ConsoleAppender.
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+
+# console uses PatternLayout.
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern= %-5p %d{ISO8601} %m - %c%n
diff --git a/org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/BasicTestConstants.java b/org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/BasicTestConstants.java
new file mode 100644 (file)
index 0000000..98b8bc9
--- /dev/null
@@ -0,0 +1,9 @@
+package org.argeo.osgi.useradmin;
+
+interface BasicTestConstants {
+       String BASE_DN = "dc=example,dc=com";
+       String ROOT_USER_DN = "uid=root,ou=users," + BASE_DN;
+       String DEMO_USER_DN = "uid=demo,ou=users," + BASE_DN;
+       String ADMIN_GROUP_DN = "cn=admin,ou=groups," + BASE_DN;
+       String EDITORS_GROUP_DN = "cn=editors,ou=groups," + BASE_DN;
+}
diff --git a/org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/LdifParserTest.java b/org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/LdifParserTest.java
new file mode 100644 (file)
index 0000000..75bfe11
--- /dev/null
@@ -0,0 +1,50 @@
+package org.argeo.osgi.useradmin;
+
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.SortedMap;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.LdifParser;
+
+import junit.framework.TestCase;
+
+public class LdifParserTest extends TestCase implements BasicTestConstants {
+       public void testBasicLdif() throws Exception {
+               LdifParser ldifParser = new LdifParser();
+               SortedMap<LdapName, Attributes> res = ldifParser.read(getClass()
+                               .getResourceAsStream("basic.ldif"));
+               LdapName rootDn = new LdapName(ROOT_USER_DN);
+               Attributes rootAttributes = res.get(rootDn);
+               assertNotNull(rootAttributes);
+               assertEquals("Superuser",
+                               rootAttributes.get(LdapAttrs.description.name()).get());
+               byte[] rawPwEntry = (byte[]) rootAttributes.get(
+                               LdapAttrs.userPassword.name()).get();
+               assertEquals("{SHA}ieSV55Qc+eQOaYDRSha/AjzNTJE=",
+                               new String(rawPwEntry));
+               byte[] hashedPassword = DigestUtils.sha1("demo".getBytes());
+               assertEquals("{SHA}" + Base64.getEncoder().encodeToString(hashedPassword),
+                               new String(rawPwEntry));
+
+               LdapName adminDn = new LdapName(ADMIN_GROUP_DN);
+               Attributes adminAttributes = res.get(adminDn);
+               assertNotNull(adminAttributes);
+               Attribute memberAttribute = adminAttributes.get(LdapAttrs.member.name());
+               assertNotNull(memberAttribute);
+               NamingEnumeration<?> members = memberAttribute.getAll();
+               List<String> users = new ArrayList<String>();
+               while (members.hasMore()) {
+                       Object value = members.next();
+                       users.add(value.toString());
+               }
+               assertEquals(1, users.size());
+               assertEquals(rootDn, new LdapName(users.get(0)));
+       }
+}
diff --git a/org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/LdifUserAdminTest.java b/org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/LdifUserAdminTest.java
new file mode 100644 (file)
index 0000000..956bb2e
--- /dev/null
@@ -0,0 +1,227 @@
+package org.argeo.osgi.useradmin;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+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;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.UUID;
+
+import javax.transaction.TransactionManager;
+
+import org.argeo.naming.LdapAttrs;
+import org.argeo.transaction.simple.SimpleTransactionManager;
+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 bitronix.tm.BitronixTransactionManager;
+import bitronix.tm.TransactionManagerServices;
+import bitronix.tm.resource.ehcache.EhCacheXAResourceProducer;
+import junit.framework.TestCase;
+
+public class LdifUserAdminTest extends TestCase implements BasicTestConstants {
+       final static int TM_SIMPLE = 0;
+       final static int TM_BITRONIX = 1;
+
+       private int tmType = TM_SIMPLE;
+       private TransactionManager tm;
+       private URI uri;
+       private AbstractUserDirectory userAdmin;
+       private Path tempDir;
+
+       // public void testConcurrent() throws Exception {
+       // }
+
+       @SuppressWarnings("unchecked")
+       public void testEdition() throws Exception {
+               User demoUser = (User) userAdmin.getRole(DEMO_USER_DN);
+               assertNotNull(demoUser);
+
+               tm.begin();
+               String newName = "demo";
+               demoUser.getProperties().put("cn", newName);
+               assertEquals(newName, demoUser.getProperties().get("cn"));
+               tm.commit();
+               persistAndRestart();
+               assertEquals(newName, demoUser.getProperties().get("cn"));
+
+               tm.begin();
+               userAdmin.removeRole(DEMO_USER_DN);
+               tm.commit();
+               persistAndRestart();
+
+               // check data
+               Role[] search = userAdmin.getRoles("(objectclass=inetOrgPerson)");
+               assertEquals(1, search.length);
+               Group editorGroup = (Group) userAdmin.getRole(EDITORS_GROUP_DN);
+               assertNotNull(editorGroup);
+               Role[] members = editorGroup.getMembers();
+               assertEquals(1, members.length);
+       }
+
+       public void testRetrieve() throws Exception {
+               // users
+               User rootUser = (User) userAdmin.getRole(ROOT_USER_DN);
+               assertNotNull(rootUser);
+               User demoUser = (User) userAdmin.getRole(DEMO_USER_DN);
+               assertNotNull(demoUser);
+
+               // groups
+               Group adminGroup = (Group) userAdmin.getRole(ADMIN_GROUP_DN);
+               assertNotNull(adminGroup);
+               Role[] members = adminGroup.getMembers();
+               assertEquals(1, members.length);
+               assertEquals(rootUser, members[0]);
+
+               Group editorGroup = (Group) userAdmin.getRole(EDITORS_GROUP_DN);
+               assertNotNull(editorGroup);
+               members = editorGroup.getMembers();
+               assertEquals(2, members.length);
+               assertEquals(adminGroup, members[0]);
+               assertEquals(demoUser, members[1]);
+
+               Authorization rootAuth = userAdmin.getAuthorization(rootUser);
+               List<String> rootRoles = Arrays.asList(rootAuth.getRoles());
+               assertEquals(3, rootRoles.size());
+               assertTrue(rootRoles.contains(ROOT_USER_DN));
+               assertTrue(rootRoles.contains(ADMIN_GROUP_DN));
+               assertTrue(rootRoles.contains(EDITORS_GROUP_DN));
+
+               // properties
+               assertEquals("root@localhost", rootUser.getProperties().get("mail"));
+
+               // credentials
+               byte[] hashedPassword = ("{SHA}" + Base64.getEncoder().encodeToString(DigestUtils.sha1("demo".getBytes())))
+                               .getBytes();
+               assertTrue(rootUser.hasCredential(LdapAttrs.userPassword.name(), hashedPassword));
+               assertTrue(demoUser.hasCredential(LdapAttrs.userPassword.name(), hashedPassword));
+
+               // search
+               Role[] search = userAdmin.getRoles(null);
+               assertEquals(4, search.length);
+               search = userAdmin.getRoles("(objectClass=groupOfNames)");
+               assertEquals(2, search.length);
+               search = userAdmin.getRoles("(objectclass=inetOrgPerson)");
+               assertEquals(2, search.length);
+               search = userAdmin.getRoles("(&(objectclass=inetOrgPerson)(uid=demo))");
+               assertEquals(1, search.length);
+       }
+
+       public void testReadWriteRead() throws Exception {
+               if (userAdmin instanceof LdifUserAdmin) {
+                       Dictionary<String, Object> props = userAdmin.getProperties();
+                       ByteArrayOutputStream out = new ByteArrayOutputStream();
+                       ((LdifUserAdmin) userAdmin).save(out);
+                       byte[] arr = out.toByteArray();
+                       out.close();
+                       userAdmin.destroy();
+                       // String written = new String(arr);
+                       // System.out.print(written);
+                       try (ByteArrayInputStream in = new ByteArrayInputStream(arr)) {
+                               userAdmin = new LdifUserAdmin(props);
+                               ((LdifUserAdmin) userAdmin).load(in);
+                       }
+                       Role[] search = userAdmin.getRoles(null);
+                       assertEquals(4, search.length);
+               } else {
+                       // test not relevant for LDAP
+               }
+       }
+
+       @Override
+       protected void setUp() throws Exception {
+               tempDir = Files.createTempDirectory(getClass().getName());
+               tempDir.toFile().deleteOnExit();
+               String uriProp = System.getProperty("argeo.userdirectory.uri");
+               if (uriProp != null)
+                       uri = new URI(uriProp);
+               else {
+                       tempDir.toFile().deleteOnExit();
+                       Path ldifPath = tempDir.resolve(BASE_DN + ".ldif");
+                       try (InputStream in = getClass().getResource("basic.ldif").openStream()) {
+                               Files.copy(in, ldifPath);
+                       }
+                       uri = ldifPath.toUri();
+               }
+
+               // Init transaction manager
+               if (TM_SIMPLE == tmType) {
+                       tm = new SimpleTransactionManager();
+               } else if (TM_BITRONIX == tmType) {
+                       bitronix.tm.Configuration tmConf = TransactionManagerServices.getConfiguration();
+                       tmConf.setServerId(UUID.randomUUID().toString());
+                       tmConf.setLogPart1Filename(new File(tempDir.toFile(), "btm1.tlog").getAbsolutePath());
+                       tmConf.setLogPart2Filename(new File(tempDir.toFile(), "btm2.tlog").getAbsolutePath());
+                       tm = TransactionManagerServices.getTransactionManager();
+               }
+
+               userAdmin = initUserAdmin(uri, tm);
+       }
+
+       private AbstractUserDirectory initUserAdmin(URI uri, TransactionManager tm) {
+               Dictionary<String, Object> props = new Hashtable<>();
+               props.put(UserAdminConf.uri.name(), uri.toString());
+               props.put(UserAdminConf.baseDn.name(), BASE_DN);
+               props.put(UserAdminConf.userBase.name(), "ou=users");
+               props.put(UserAdminConf.groupBase.name(), "ou=groups");
+               AbstractUserDirectory userAdmin;
+               if (uri.getScheme().startsWith("ldap"))
+                       userAdmin = new LdapUserAdmin(props);
+               else
+                       userAdmin = new LdifUserAdmin(props);
+               userAdmin.init();
+               // JTA
+               if (TM_BITRONIX == tmType)
+                       EhCacheXAResourceProducer.registerXAResource(UserDirectory.class.getName(), userAdmin.getXaResource());
+               userAdmin.setTransactionManager(tm);
+               return userAdmin;
+       }
+
+       private void persistAndRestart() {
+               if (TM_BITRONIX == tmType)
+                       EhCacheXAResourceProducer.unregisterXAResource(UserDirectory.class.getName(), userAdmin.getXaResource());
+               if (userAdmin instanceof LdifUserAdmin)
+                       ((LdifUserAdmin) userAdmin).save();
+               userAdmin.destroy();
+               userAdmin = initUserAdmin(uri, tm);
+       }
+
+       @Override
+       protected void tearDown() throws Exception {
+               if (TM_BITRONIX == tmType) {
+                       EhCacheXAResourceProducer.unregisterXAResource(UserDirectory.class.getName(), userAdmin.getXaResource());
+                       ((BitronixTransactionManager) tm).shutdown();
+               }
+               if (userAdmin != null)
+                       userAdmin.destroy();
+               if (tempDir != null)
+                       Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
+                               @Override
+                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                       Files.delete(file);
+                                       return FileVisitResult.CONTINUE;
+                               }
+
+                               @Override
+                               public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+                                       Files.delete(dir);
+                                       return FileVisitResult.CONTINUE;
+                               }
+
+                       });
+       }
+
+}
diff --git a/org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/UserAdminConfTest.java b/org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/UserAdminConfTest.java
new file mode 100644 (file)
index 0000000..d69cae4
--- /dev/null
@@ -0,0 +1,53 @@
+package org.argeo.osgi.useradmin;
+
+import static org.argeo.osgi.useradmin.UserAdminConf.propertiesAsUri;
+import static org.argeo.osgi.useradmin.UserAdminConf.uriAsProperties;
+
+import java.net.URI;
+import java.util.Dictionary;
+
+import junit.framework.TestCase;
+
+public class UserAdminConfTest extends TestCase {
+       public void testUriFormat() throws Exception {
+               // LDAP
+               URI uriIn = new URI("ldap://" + "uid=admin,ou=system:secret@localhost:10389" + "/dc=example,dc=com"
+                               + "?readOnly=false&userObjectClass=person");
+               Dictionary<String, ?> props = uriAsProperties(uriIn.toString());
+               System.out.println(props);
+               assertEquals("dc=example,dc=com", props.get(UserAdminConf.baseDn.name()));
+               assertEquals("false", props.get(UserAdminConf.readOnly.name()));
+               assertEquals("person", props.get(UserAdminConf.userObjectClass.name()));
+               URI uriOut = propertiesAsUri(props);
+               System.out.println(uriOut);
+               assertEquals("/dc=example,dc=com?userObjectClass=person&readOnly=false", uriOut.toString());
+
+               // File
+               uriIn = new URI("file://some/dir/dc=example,dc=com.ldif");
+               props = uriAsProperties(uriIn.toString());
+               System.out.println(props);
+               assertEquals("dc=example,dc=com", props.get(UserAdminConf.baseDn.name()));
+
+               // Base configuration
+               uriIn = new URI("/dc=example,dc=com.ldif?readOnly=true&userBase=ou=CoWorkers,ou=People&groupBase=ou=Roles");
+               props = uriAsProperties(uriIn.toString());
+               System.out.println(props);
+               assertEquals("dc=example,dc=com", props.get(UserAdminConf.baseDn.name()));
+               assertEquals("true", props.get(UserAdminConf.readOnly.name()));
+               assertEquals("ou=CoWorkers,ou=People", props.get(UserAdminConf.userBase.name()));
+               assertEquals("ou=Roles", props.get(UserAdminConf.groupBase.name()));
+               uriOut = propertiesAsUri(props);
+               System.out.println(uriOut);
+               assertEquals("/dc=example,dc=com?userBase=ou=CoWorkers,ou=People&groupBase=ou=Roles&readOnly=true", uriOut.toString());
+
+               // OS
+               uriIn = new URI("os:///dc=example,dc=com");
+               props = uriAsProperties(uriIn.toString());
+               System.out.println(props);
+               assertEquals("dc=example,dc=com", props.get(UserAdminConf.baseDn.name()));
+               assertEquals("true", props.get(UserAdminConf.readOnly.name()));
+               uriOut = propertiesAsUri(props);
+               System.out.println(uriOut);
+               assertEquals("/dc=example,dc=com?readOnly=true", uriOut.toString());
+       }
+}
diff --git a/org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/basic.ldif b/org.argeo.enterprise/ext/test/org/argeo/osgi/useradmin/basic.ldif
new file mode 100644 (file)
index 0000000..b7328b0
--- /dev/null
@@ -0,0 +1,54 @@
+dn: dc=example,dc=com
+objectClass: domain
+objectClass: extensibleObject
+objectClass: top
+dc: example
+
+dn: ou=groups,dc=example,dc=com
+objectClass: organizationalUnit
+objectClass: top
+ou: groups
+
+dn: ou=users,dc=example,dc=com
+objectClass: organizationalUnit
+objectClass: top
+ou: users
+
+dn: uid=demo,ou=users,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Demo User
+description: Demo user
+givenName: Demo
+mail: demo@localhost
+sn: User
+uid: demo
+userPassword:: e1NIQX1pZVNWNTVRYytlUU9hWURSU2hhL0Fqek5USkU9
+
+dn: uid=root,ou=users,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: person
+objectClass: organizationalPerson
+objectClass: top
+cn: Super User
+description: Superuser
+givenName: Super
+mail: root@localhost
+sn: User
+uid: root
+userPassword:: e1NIQX1pZVNWNTVRYytlUU9hWURSU2hhL0Fqek5USkU9
+
+dn: cn=admin,ou=groups,dc=example,dc=com
+objectClass: groupOfNames
+objectClass: top
+cn: admin
+member: uid=root,ou=users,dc=example,dc=com
+
+dn: cn=editors,ou=groups,dc=example,dc=com
+objectClass: groupOfNames
+objectClass: top
+cn: editors
+member: cn=admin,ou=groups,dc=example,dc=com
+member: uid=demo,ou=users,dc=example,dc=com
diff --git a/org.argeo.enterprise/pom.xml b/org.argeo.enterprise/pom.xml
new file mode 100644 (file)
index 0000000..8ed12b7
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.enterprise</artifactId>
+       <name>Commons Enterprise</name>
+</project>
\ No newline at end of file
diff --git a/org.argeo.enterprise/src/org/argeo/naming/AttributesDictionary.java b/org.argeo.enterprise/src/org/argeo/naming/AttributesDictionary.java
new file mode 100644 (file)
index 0000000..e047216
--- /dev/null
@@ -0,0 +1,171 @@
+package org.argeo.naming;
+
+import java.util.Dictionary;
+import java.util.Enumeration;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+
+public class AttributesDictionary extends Dictionary<String, Object> {
+       private final Attributes attributes;
+
+       /** The provided attributes is wrapped, not copied. */
+       public AttributesDictionary(Attributes attributes) {
+               if (attributes == null)
+                       throw new IllegalArgumentException("Attributes cannot be null");
+               this.attributes = attributes;
+       }
+
+       @Override
+       public int size() {
+               return attributes.size();
+       }
+
+       @Override
+       public boolean isEmpty() {
+               return attributes.size() == 0;
+       }
+
+       @Override
+       public Enumeration<String> keys() {
+               NamingEnumeration<String> namingEnumeration = attributes.getIDs();
+               return new Enumeration<String>() {
+
+                       @Override
+                       public boolean hasMoreElements() {
+                               return namingEnumeration.hasMoreElements();
+                       }
+
+                       @Override
+                       public String nextElement() {
+                               return namingEnumeration.nextElement();
+                       }
+
+               };
+       }
+
+       @Override
+       public Enumeration<Object> elements() {
+               NamingEnumeration<String> namingEnumeration = attributes.getIDs();
+               return new Enumeration<Object>() {
+
+                       @Override
+                       public boolean hasMoreElements() {
+                               return namingEnumeration.hasMoreElements();
+                       }
+
+                       @Override
+                       public Object nextElement() {
+                               String key = namingEnumeration.nextElement();
+                               return get(key);
+                       }
+
+               };
+       }
+
+       @Override
+       /** @returns a <code>String</code> or <code>String[]</code> */
+       public Object get(Object key) {
+               try {
+                       if (key == null)
+                               throw new IllegalArgumentException("Key cannot be null");
+                       Attribute attr = attributes.get(key.toString());
+                       if (attr == null)
+                               return null;
+                       if (attr.size() == 0)
+                               throw new IllegalStateException("There must be at least one value");
+                       else if (attr.size() == 1) {
+                               return attr.get().toString();
+                       } else {// multiple
+                               String[] res = new String[attr.size()];
+                               for (int i = 0; i < attr.size(); i++) {
+                                       Object value = attr.get();
+                                       if (value == null)
+                                               throw new RuntimeException("Values cannot be null");
+                                       res[i] = attr.get(i).toString();
+                               }
+                               return res;
+                       }
+               } catch (NamingException e) {
+                       throw new RuntimeException("Cannot get value for " + key, e);
+               }
+       }
+
+       @Override
+       public Object put(String key, Object value) {
+               if (key == null)
+                       throw new IllegalArgumentException("Key cannot be null");
+               if (value == null)
+                       throw new IllegalArgumentException("Value cannot be null");
+
+               Object oldValue = get(key);
+               Attribute attr = attributes.get(key);
+               if (attr == null) {
+                       attr = new BasicAttribute(key);
+                       attributes.put(attr);
+               }
+
+               if (value instanceof String[]) {
+                       String[] values = (String[]) value;
+                       // clean additional values
+                       for (int i = values.length; i < attr.size(); i++)
+                               attr.remove(i);
+                       // set values
+                       for (int i = 0; i < values.length; i++) {
+                               attr.set(i, values[i]);
+                       }
+               } else {
+                       if (attr.size() > 1)
+                               throw new IllegalArgumentException("Attribute " + key + " is multi-valued");
+                       if (attr.size() == 1) {
+                               try {
+                                       if (!attr.get(0).equals(value))
+                                               attr.set(0, value.toString());
+                               } catch (NamingException e) {
+                                       throw new RuntimeException("Cannot check existing value", e);
+                               }
+                       } else {
+                               attr.add(value.toString());
+                       }
+               }
+               return oldValue;
+       }
+
+       @Override
+       public Object remove(Object key) {
+               if (key == null)
+                       throw new IllegalArgumentException("Key cannot be null");
+               Object oldValue = get(key);
+               if (oldValue == null)
+                       return null;
+               return attributes.remove(key.toString());
+       }
+
+       /**
+        * Copy the <b>content</b> of an {@link Attributes} to the provided
+        * {@link Dictionary}.
+        */
+       public static void copy(Attributes attributes, Dictionary<String, Object> dictionary) {
+               AttributesDictionary ad = new AttributesDictionary(attributes);
+               Enumeration<String> keys = ad.keys();
+               while (keys.hasMoreElements()) {
+                       String key = keys.nextElement();
+                       dictionary.put(key, ad.get(key));
+               }
+       }
+
+       /**
+        * Copy a {@link Dictionary} into an {@link Attributes}.
+        */
+       public static void copy(Dictionary<String, Object> dictionary, Attributes attributes) {
+               AttributesDictionary ad = new AttributesDictionary(attributes);
+               Enumeration<String> keys = dictionary.keys();
+               while (keys.hasMoreElements()) {
+                       String key = keys.nextElement();
+                       ad.put(key, dictionary.get(key));
+               }
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/naming/AuthPassword.java b/org.argeo.enterprise/src/org/argeo/naming/AuthPassword.java
new file mode 100644 (file)
index 0000000..6d4c62b
--- /dev/null
@@ -0,0 +1,140 @@
+package org.argeo.naming;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.StringTokenizer;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import org.argeo.osgi.useradmin.UserDirectoryException;
+
+/** LDAP authPassword field according to RFC 3112 */
+public class AuthPassword implements CallbackHandler {
+       private final String authScheme;
+       private final String authInfo;
+       private final String authValue;
+
+       public AuthPassword(String value) {
+               StringTokenizer st = new StringTokenizer(value, "$");
+               // TODO make it more robust, deal with bad formatting
+               this.authScheme = st.nextToken().trim();
+               this.authInfo = st.nextToken().trim();
+               this.authValue = st.nextToken().trim();
+
+               String expectedAuthScheme = getExpectedAuthScheme();
+               if (expectedAuthScheme != null && !authScheme.equals(expectedAuthScheme))
+                       throw new IllegalArgumentException(
+                                       "Auth scheme " + authScheme + " is not compatible with " + expectedAuthScheme);
+       }
+
+       protected AuthPassword(String authInfo, String authValue) {
+               this.authScheme = getExpectedAuthScheme();
+               if (authScheme == null)
+                       throw new IllegalArgumentException("Expected auth scheme cannot be null");
+               this.authInfo = authInfo;
+               this.authValue = authValue;
+       }
+
+       protected AuthPassword(AuthPassword authPassword) {
+               this.authScheme = authPassword.getAuthScheme();
+               this.authInfo = authPassword.getAuthInfo();
+               this.authValue = authPassword.getAuthValue();
+       }
+
+       protected String getExpectedAuthScheme() {
+               return null;
+       }
+
+       protected boolean matchAuthValue(Object object) {
+               return authValue.equals(object.toString());
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (!(obj instanceof AuthPassword))
+                       return false;
+               AuthPassword authPassword = (AuthPassword) obj;
+               return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo)
+                               && authValue.equals(authValue);
+       }
+
+       public boolean keyEquals(AuthPassword authPassword) {
+               return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo);
+       }
+
+       @Override
+       public int hashCode() {
+               return authValue.hashCode();
+       }
+
+       @Override
+       public String toString() {
+               return toAuthPassword();
+       }
+
+       public final String toAuthPassword() {
+               return getAuthScheme() + '$' + authInfo + '$' + authValue;
+       }
+
+       public String getAuthScheme() {
+               return authScheme;
+       }
+
+       public String getAuthInfo() {
+               return authInfo;
+       }
+
+       public String getAuthValue() {
+               return authValue;
+       }
+
+       public static AuthPassword matchAuthValue(Attributes attributes, char[] value) {
+               try {
+                       Attribute authPassword = attributes.get(LdapAttrs.authPassword.name());
+                       if (authPassword != null) {
+                               NamingEnumeration<?> values = authPassword.getAll();
+                               while (values.hasMore()) {
+                                       Object val = values.next();
+                                       AuthPassword token = new AuthPassword(val.toString());
+                                       String auth;
+                                       if (Arrays.binarySearch(value, '$') >= 0) {
+                                               auth = token.authInfo + '$' + token.authValue;
+                                       } else {
+                                               auth = token.authValue;
+                                       }
+                                       if (Arrays.equals(auth.toCharArray(), value))
+                                               return token;
+                                       // if (token.matchAuthValue(value))
+                                       // return token;
+                               }
+                       }
+                       return null;
+               } catch (NamingException e) {
+                       throw new UserDirectoryException("Cannot check attribute", e);
+               }
+       }
+
+       public static boolean remove(Attributes attributes, AuthPassword value) {
+               Attribute authPassword = attributes.get(LdapAttrs.authPassword.name());
+               return authPassword.remove(value.toAuthPassword());
+       }
+
+       @Override
+       public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
+               for (Callback callback : callbacks) {
+                       if (callback instanceof NameCallback)
+                               ((NameCallback) callback).setName(toAuthPassword());
+                       else if (callback instanceof PasswordCallback)
+                               ((PasswordCallback) callback).setPassword(getAuthValue().toCharArray());
+               }
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/naming/DnsBrowser.java b/org.argeo.enterprise/src/org/argeo/naming/DnsBrowser.java
new file mode 100644 (file)
index 0000000..7aadd64
--- /dev/null
@@ -0,0 +1,184 @@
+package org.argeo.naming;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import javax.naming.Binding;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class DnsBrowser implements Closeable {
+       private final static Log log = LogFactory.getLog(DnsBrowser.class);
+
+       private final DirContext initialCtx;
+
+       public DnsBrowser() throws NamingException {
+               this(null);
+       }
+
+       public DnsBrowser(String dnsServerUrls) throws NamingException {
+               Hashtable<String, Object> env = new Hashtable<>();
+               env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
+               if (dnsServerUrls != null)
+                       env.put("java.naming.provider.url", dnsServerUrls);
+               initialCtx = new InitialDirContext(env);
+       }
+
+       public Map<String, List<String>> getAllRecords(String name) throws NamingException {
+               Map<String, List<String>> res = new TreeMap<>();
+               Attributes attrs = initialCtx.getAttributes(name);
+               NamingEnumeration<String> ids = attrs.getIDs();
+               while (ids.hasMore()) {
+                       String recordType = ids.next();
+                       List<String> lst = new ArrayList<String>();
+                       res.put(recordType, lst);
+                       Attribute attr = attrs.get(recordType);
+                       addValues(attr, lst);
+               }
+               return Collections.unmodifiableMap(res);
+       }
+
+       /**
+        * Return a single record (typically A, AAAA, etc. or null if not available.
+        * Will fail if multiple records.
+        */
+       public String getRecord(String name, String recordType) throws NamingException {
+               Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType });
+               if (attrs.size() == 0)
+                       return null;
+               Attribute attr = attrs.get(recordType);
+               if (attr.size() > 1)
+                       throw new IllegalArgumentException("Multiple record type " + recordType);
+               assert attr.size() != 0;
+               Object value = attr.get();
+               assert value != null;
+               return value.toString();
+       }
+
+       /**
+        * Return records of a given type.
+        */
+       public List<String> getRecords(String name, String recordType) throws NamingException {
+               List<String> res = new ArrayList<String>();
+               Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType });
+               Attribute attr = attrs.get(recordType);
+               addValues(attr, res);
+               return res;
+       }
+
+       /** Ordered, with preferred first. */
+       public List<String> getSrvRecordsAsHosts(String name) throws NamingException {
+               List<String> raw = getRecords(name, "SRV");
+               if (raw.size() == 0)
+                       return null;
+               SortedSet<SrvRecord> res = new TreeSet<>();
+               for (int i = 0; i < raw.size(); i++) {
+                       String record = raw.get(i);
+                       String[] arr = record.split(" ");
+                       Integer priority = Integer.parseInt(arr[0]);
+                       Integer weight = Integer.parseInt(arr[1]);
+                       Integer port = Integer.parseInt(arr[2]);
+                       String hostname = arr[3];
+                       SrvRecord order = new SrvRecord(priority, weight, port, hostname);
+                       res.add(order);
+               }
+               List<String> lst = new ArrayList<>();
+               for (SrvRecord order : res) {
+                       lst.add(order.toHost());
+               }
+               return Collections.unmodifiableList(lst);
+       }
+
+       private void addValues(Attribute attr, List<String> lst) throws NamingException {
+               NamingEnumeration<?> values = attr.getAll();
+               while (values.hasMore()) {
+                       Object value = values.next();
+                       if (value != null) {
+                               if (value instanceof byte[]) {
+                                       String str = Base64.getEncoder().encodeToString((byte[]) value);
+                                       lst.add(str);
+                               } else
+                                       lst.add(value.toString());
+                       }
+               }
+
+       }
+
+       public List<String> listEntries(String name) throws NamingException {
+               List<String> res = new ArrayList<String>();
+               NamingEnumeration<Binding> ne = initialCtx.listBindings(name);
+               while (ne.hasMore()) {
+                       Binding b = ne.next();
+                       res.add(b.getName());
+               }
+               return Collections.unmodifiableList(res);
+       }
+
+       @Override
+       public void close() throws IOException {
+               destroy();
+       }
+
+       public void destroy() {
+               try {
+                       initialCtx.close();
+               } catch (NamingException e) {
+                       log.error("Cannot close context", e);
+               }
+       }
+
+       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.enterprise/src/org/argeo/naming/LdapAttrs.csv b/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.csv
new file mode 100644 (file)
index 0000000..676d727
--- /dev/null
@@ -0,0 +1,129 @@
+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.enterprise/src/org/argeo/naming/LdapAttrs.java b/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.java
new file mode 100644 (file)
index 0000000..171b83a
--- /dev/null
@@ -0,0 +1,320 @@
+package org.argeo.naming;
+
+/**
+ * Standard LDAP attributes as per:<br>
+ * - <a href= "https://www.ldap.com/ldap-oid-reference">Standard LDAP</a><br>
+ * - <a href=
+ * "https://github.com/krb5/krb5/blob/master/src/plugins/kdb/ldap/libkdb_ldap/kerberos.schema">Kerberos
+ * LDAP (partial)</a>
+ */
+public enum LdapAttrs implements SpecifiedName {
+       /** */
+       uid("0.9.2342.19200300.100.1.1", "RFC 4519"),
+       /** */
+       mail("0.9.2342.19200300.100.1.3", "RFC 4524"),
+       /** */
+       info("0.9.2342.19200300.100.1.4", "RFC 4524"),
+       /** */
+       drink("0.9.2342.19200300.100.1.5", "RFC 4524"),
+       /** */
+       roomNumber("0.9.2342.19200300.100.1.6", "RFC 4524"),
+       /** */
+       photo("0.9.2342.19200300.100.1.7", "RFC 2798"),
+       /** */
+       userClass("0.9.2342.19200300.100.1.8", "RFC 4524"),
+       /** */
+       host("0.9.2342.19200300.100.1.9", "RFC 4524"),
+       /** */
+       manager("0.9.2342.19200300.100.1.10", "RFC 4524"),
+       /** */
+       documentIdentifier("0.9.2342.19200300.100.1.11", "RFC 4524"),
+       /** */
+       documentTitle("0.9.2342.19200300.100.1.12", "RFC 4524"),
+       /** */
+       documentVersion("0.9.2342.19200300.100.1.13", "RFC 4524"),
+       /** */
+       documentAuthor("0.9.2342.19200300.100.1.14", "RFC 4524"),
+       /** */
+       documentLocation("0.9.2342.19200300.100.1.15", "RFC 4524"),
+       /** */
+       homePhone("0.9.2342.19200300.100.1.20", "RFC 4524"),
+       /** */
+       secretary("0.9.2342.19200300.100.1.21", "RFC 4524"),
+       /** */
+       dc("0.9.2342.19200300.100.1.25", "RFC 4519"),
+       /** */
+       associatedDomain("0.9.2342.19200300.100.1.37", "RFC 4524"),
+       /** */
+       associatedName("0.9.2342.19200300.100.1.38", "RFC 4524"),
+       /** */
+       homePostalAddress("0.9.2342.19200300.100.1.39", "RFC 4524"),
+       /** */
+       personalTitle("0.9.2342.19200300.100.1.40", "RFC 4524"),
+       /** */
+       mobile("0.9.2342.19200300.100.1.41", "RFC 4524"),
+       /** */
+       pager("0.9.2342.19200300.100.1.42", "RFC 4524"),
+       /** */
+       co("0.9.2342.19200300.100.1.43", "RFC 4524"),
+       /** */
+       uniqueIdentifier("0.9.2342.19200300.100.1.44", "RFC 4524"),
+       /** */
+       organizationalStatus("0.9.2342.19200300.100.1.45", "RFC 4524"),
+       /** */
+       buildingName("0.9.2342.19200300.100.1.48", "RFC 4524"),
+       /** */
+       audio("0.9.2342.19200300.100.1.55", "RFC 2798"),
+       /** */
+       documentPublisher("0.9.2342.19200300.100.1.56", "RFC 4524"),
+       /** */
+       jpegPhoto("0.9.2342.19200300.100.1.60", "RFC 2798"),
+       /** */
+       vendorName("1.3.6.1.1.4", "RFC 3045"),
+       /** */
+       vendorVersion("1.3.6.1.1.5", "RFC 3045"),
+       /** */
+       entryUUID("1.3.6.1.1.16.4", "RFC 4530"),
+       /** */
+       entryDN("1.3.6.1.1.20", "RFC 5020"),
+       /** */
+       labeledURI("1.3.6.1.4.1.250.1.57", "RFC 2798"),
+       /** */
+       numSubordinates("1.3.6.1.4.1.453.16.2.103", "draft-ietf-boreham-numsubordinates"),
+       /** */
+       namingContexts("1.3.6.1.4.1.1466.101.120.5", "RFC 4512"),
+       /** */
+       altServer("1.3.6.1.4.1.1466.101.120.6", "RFC 4512"),
+       /** */
+       supportedExtension("1.3.6.1.4.1.1466.101.120.7", "RFC 4512"),
+       /** */
+       supportedControl("1.3.6.1.4.1.1466.101.120.13", "RFC 4512"),
+       /** */
+       supportedSASLMechanisms("1.3.6.1.4.1.1466.101.120.14", "RFC 4512"),
+       /** */
+       supportedLDAPVersion("1.3.6.1.4.1.1466.101.120.15", "RFC 4512"),
+       /** */
+       ldapSyntaxes("1.3.6.1.4.1.1466.101.120.16", "RFC 4512"),
+       /** */
+       supportedAuthPasswordSchemes("1.3.6.1.4.1.4203.1.3.3", "RFC 3112"),
+       /** */
+       authPassword("1.3.6.1.4.1.4203.1.3.4", "RFC 3112"),
+       /** */
+       supportedFeatures("1.3.6.1.4.1.4203.1.3.5", "RFC 4512"),
+       /** */
+       inheritable("1.3.6.1.4.1.7628.5.4.1", "draft-ietf-ldup-subentry"),
+       /** */
+       blockInheritance("1.3.6.1.4.1.7628.5.4.2", "draft-ietf-ldup-subentry"),
+       /** */
+       objectClass("2.5.4.0", "RFC 4512"),
+       /** */
+       aliasedObjectName("2.5.4.1", "RFC 4512"),
+       /** */
+       cn("2.5.4.3", "RFC 4519"),
+       /** */
+       sn("2.5.4.4", "RFC 4519"),
+       /** */
+       serialNumber("2.5.4.5", "RFC 4519"),
+       /** */
+       c("2.5.4.6", "RFC 4519"),
+       /** */
+       l("2.5.4.7", "RFC 4519"),
+       /** */
+       st("2.5.4.8", "RFC 4519"),
+       /** */
+       street("2.5.4.9", "RFC 4519"),
+       /** */
+       o("2.5.4.10", "RFC 4519"),
+       /** */
+       ou("2.5.4.11", "RFC 4519"),
+       /** */
+       title("2.5.4.12", "RFC 4519"),
+       /** */
+       description("2.5.4.13", "RFC 4519"),
+       /** */
+       searchGuide("2.5.4.14", "RFC 4519"),
+       /** */
+       businessCategory("2.5.4.15", "RFC 4519"),
+       /** */
+       postalAddress("2.5.4.16", "RFC 4519"),
+       /** */
+       postalCode("2.5.4.17", "RFC 4519"),
+       /** */
+       postOfficeBox("2.5.4.18", "RFC 4519"),
+       /** */
+       physicalDeliveryOfficeName("2.5.4.19", "RFC 4519"),
+       /** */
+       telephoneNumber("2.5.4.20", "RFC 4519"),
+       /** */
+       telexNumber("2.5.4.21", "RFC 4519"),
+       /** */
+       teletexTerminalIdentifier("2.5.4.22", "RFC 4519"),
+       /** */
+       facsimileTelephoneNumber("2.5.4.23", "RFC 4519"),
+       /** */
+       x121Address("2.5.4.24", "RFC 4519"),
+       /** */
+       internationalISDNNumber("2.5.4.25", "RFC 4519"),
+       /** */
+       registeredAddress("2.5.4.26", "RFC 4519"),
+       /** */
+       destinationIndicator("2.5.4.27", "RFC 4519"),
+       /** */
+       preferredDeliveryMethod("2.5.4.28", "RFC 4519"),
+       /** */
+       member("2.5.4.31", "RFC 4519"),
+       /** */
+       owner("2.5.4.32", "RFC 4519"),
+       /** */
+       roleOccupant("2.5.4.33", "RFC 4519"),
+       /** */
+       seeAlso("2.5.4.34", "RFC 4519"),
+       /** */
+       userPassword("2.5.4.35", "RFC 4519"),
+       /** */
+       userCertificate("2.5.4.36", "RFC 4523"),
+       /** */
+       cACertificate("2.5.4.37", "RFC 4523"),
+       /** */
+       authorityRevocationList("2.5.4.38", "RFC 4523"),
+       /** */
+       certificateRevocationList("2.5.4.39", "RFC 4523"),
+       /** */
+       crossCertificatePair("2.5.4.40", "RFC 4523"),
+       /** */
+       name("2.5.4.41", "RFC 4519"),
+       /** */
+       givenName("2.5.4.42", "RFC 4519"),
+       /** */
+       initials("2.5.4.43", "RFC 4519"),
+       /** */
+       generationQualifier("2.5.4.44", "RFC 4519"),
+       /** */
+       x500UniqueIdentifier("2.5.4.45", "RFC 4519"),
+       /** */
+       dnQualifier("2.5.4.46", "RFC 4519"),
+       /** */
+       enhancedSearchGuide("2.5.4.47", "RFC 4519"),
+       /** */
+       distinguishedName("2.5.4.49", "RFC 4519"),
+       /** */
+       uniqueMember("2.5.4.50", "RFC 4519"),
+       /** */
+       houseIdentifier("2.5.4.51", "RFC 4519"),
+       /** */
+       supportedAlgorithms("2.5.4.52", "RFC 4523"),
+       /** */
+       deltaRevocationList("2.5.4.53", "RFC 4523"),
+       /** */
+       createTimestamp("2.5.18.1", "RFC 4512"),
+       /** */
+       modifyTimestamp("2.5.18.2", "RFC 4512"),
+       /** */
+       creatorsName("2.5.18.3", "RFC 4512"),
+       /** */
+       modifiersName("2.5.18.4", "RFC 4512"),
+       /** */
+       subschemaSubentry("2.5.18.10", "RFC 4512"),
+       /** */
+       dITStructureRules("2.5.21.1", "RFC 4512"),
+       /** */
+       dITContentRules("2.5.21.2", "RFC 4512"),
+       /** */
+       matchingRules("2.5.21.4", "RFC 4512"),
+       /** */
+       attributeTypes("2.5.21.5", "RFC 4512"),
+       /** */
+       objectClasses("2.5.21.6", "RFC 4512"),
+       /** */
+       nameForms("2.5.21.7", "RFC 4512"),
+       /** */
+       matchingRuleUse("2.5.21.8", "RFC 4512"),
+       /** */
+       structuralObjectClass("2.5.21.9", "RFC 4512"),
+       /** */
+       governingStructureRule("2.5.21.10", "RFC 4512"),
+       /** */
+       carLicense("2.16.840.1.113730.3.1.1", "RFC 2798"),
+       /** */
+       departmentNumber("2.16.840.1.113730.3.1.2", "RFC 2798"),
+       /** */
+       employeeNumber("2.16.840.1.113730.3.1.3", "RFC 2798"),
+       /** */
+       employeeType("2.16.840.1.113730.3.1.4", "RFC 2798"),
+       /** */
+       changeNumber("2.16.840.1.113730.3.1.5", "draft-good-ldap-changelog"),
+       /** */
+       targetDN("2.16.840.1.113730.3.1.6", "draft-good-ldap-changelog"),
+       /** */
+       changeType("2.16.840.1.113730.3.1.7", "draft-good-ldap-changelog"),
+       /** */
+       changes("2.16.840.1.113730.3.1.8", "draft-good-ldap-changelog"),
+       /** */
+       newRDN("2.16.840.1.113730.3.1.9", "draft-good-ldap-changelog"),
+       /** */
+       deleteOldRDN("2.16.840.1.113730.3.1.10", "draft-good-ldap-changelog"),
+       /** */
+       newSuperior("2.16.840.1.113730.3.1.11", "draft-good-ldap-changelog"),
+       /** */
+       ref("2.16.840.1.113730.3.1.34", "RFC 3296"),
+       /** */
+       changelog("2.16.840.1.113730.3.1.35", "draft-good-ldap-changelog"),
+       /** */
+       preferredLanguage("2.16.840.1.113730.3.1.39", "RFC 2798"),
+       /** */
+       userSMIMECertificate("2.16.840.1.113730.3.1.40", "RFC 2798"),
+       /** */
+       userPKCS12("2.16.840.1.113730.3.1.216", "RFC 2798"),
+       /** */
+       displayName("2.16.840.1.113730.3.1.241", "RFC 2798"),
+
+       // Sun memberOf
+       memberOf("1.2.840.113556.1.2.102", "389 DS memberOf"),
+
+       // KERBEROS (partial)
+       krbPrincipalName("2.16.840.1.113719.1.301.6.8.1", "Novell Kerberos Schema Definitions"),
+
+       // RFC 2985 and RFC 3039 (partial)
+       dateOfBirth("1.3.6.1.5.5.7.9.1", "RFC 2985"),
+       /** */
+       placeOfBirth("1.3.6.1.5.5.7.9.2", "RFC 2985"),
+       /** */
+       gender("1.3.6.1.5.5.7.9.3", "RFC 2985"),
+       /** */
+       countryOfCitizenship("1.3.6.1.5.5.7.9.4", "RFC 2985"),
+       /** */
+       countryOfResidence("1.3.6.1.5.5.7.9.5", "RFC 2985"),
+       //
+       ;
+
+       public final static String DN = "dn";
+
+       private final static String LDAP_ = "ldap:";
+
+       private final String oid, spec;
+
+       LdapAttrs(String oid, String spec) {
+               this.oid = oid;
+               this.spec = spec;
+       }
+
+       @Override
+       public String getID() {
+               return oid;
+       }
+
+       @Override
+       public String getSpec() {
+               return spec;
+       }
+
+       public String property() {
+               return new StringBuilder(LDAP_).append(name()).toString();
+       }
+
+       @Override
+       public final String toString() {
+               // must return the name
+               return name();
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/naming/LdapObjs.csv b/org.argeo.enterprise/src/org/argeo/naming/LdapObjs.csv
new file mode 100644 (file)
index 0000000..3d907cb
--- /dev/null
@@ -0,0 +1,42 @@
+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.enterprise/src/org/argeo/naming/LdapObjs.java b/org.argeo.enterprise/src/org/argeo/naming/LdapObjs.java
new file mode 100644 (file)
index 0000000..0611675
--- /dev/null
@@ -0,0 +1,114 @@
+package org.argeo.naming;
+
+/**
+ * Standard LDAP object classes as per
+ * <a href="https://www.ldap.com/ldap-oid-reference">https://www.ldap.com/ldap-
+ * oid-reference</a>
+ */
+public enum LdapObjs implements SpecifiedName {
+       account("0.9.2342.19200300.100.4.5", "RFC 4524"),
+       /** */
+       document("0.9.2342.19200300.100.4.6", "RFC 4524"),
+       /** */
+       room("0.9.2342.19200300.100.4.7", "RFC 4524"),
+       /** */
+       documentSeries("0.9.2342.19200300.100.4.9", "RFC 4524"),
+       /** */
+       domain("0.9.2342.19200300.100.4.13", "RFC 4524"),
+       /** */
+       rFC822localPart("0.9.2342.19200300.100.4.14", "RFC 4524"),
+       /** */
+       domainRelatedObject("0.9.2342.19200300.100.4.17", "RFC 4524"),
+       /** */
+       friendlyCountry("0.9.2342.19200300.100.4.18", "RFC 4524"),
+       /** */
+       simpleSecurityObject("0.9.2342.19200300.100.4.19", "RFC 4524"),
+       /** */
+       uidObject("1.3.6.1.1.3.1", "RFC 4519"),
+       /** */
+       extensibleObject("1.3.6.1.4.1.1466.101.120.111", "RFC 4512"),
+       /** */
+       dcObject("1.3.6.1.4.1.1466.344", "RFC 4519"),
+       /** */
+       authPasswordObject("1.3.6.1.4.1.4203.1.4.7", "RFC 3112"),
+       /** */
+       namedObject("1.3.6.1.4.1.5322.13.1.1", "draft-howard-namedobject"),
+       /** */
+       inheritableLDAPSubEntry("1.3.6.1.4.1.7628.5.6.1.1", "draft-ietf-ldup-subentry"),
+       /** */
+       top("2.5.6.0", "RFC 4512"),
+       /** */
+       alias("2.5.6.1", "RFC 4512"),
+       /** */
+       country("2.5.6.2", "RFC 4519"),
+       /** */
+       locality("2.5.6.3", "RFC 4519"),
+       /** */
+       organization("2.5.6.4", "RFC 4519"),
+       /** */
+       organizationalUnit("2.5.6.5", "RFC 4519"),
+       /** */
+       person("2.5.6.6", "RFC 4519"),
+       /** */
+       organizationalPerson("2.5.6.7", "RFC 4519"),
+       /** */
+       organizationalRole("2.5.6.8", "RFC 4519"),
+       /** */
+       groupOfNames("2.5.6.9", "RFC 4519"),
+       /** */
+       residentialPerson("2.5.6.10", "RFC 4519"),
+       /** */
+       applicationProcess("2.5.6.11", "RFC 4519"),
+       /** */
+       device("2.5.6.14", "RFC 4519"),
+       /** */
+       strongAuthenticationUser("2.5.6.15", "RFC 4523"),
+       /** */
+       certificationAuthority("2.5.6.16", "RFC 4523"),
+       // /** Should be certificationAuthority-V2 */
+       // certificationAuthority_V2("2.5.6.16.2", "RFC 4523") {
+       // },
+       /** */
+       groupOfUniqueNames("2.5.6.17", "RFC 4519"),
+       /** */
+       userSecurityInformation("2.5.6.18", "RFC 4523"),
+       /** */
+       cRLDistributionPoint("2.5.6.19", "RFC 4523"),
+       /** */
+       pkiUser("2.5.6.21", "RFC 4523"),
+       /** */
+       pkiCA("2.5.6.22", "RFC 4523"),
+       /** */
+       deltaCRL("2.5.6.23", "RFC 4523"),
+       /** */
+       subschema("2.5.20.1", "RFC 4512"),
+       /** */
+       ldapSubEntry("2.16.840.1.113719.2.142.6.1.1", "draft-ietf-ldup-subentry"),
+       /** */
+       changeLogEntry("2.16.840.1.113730.3.2.1", "draft-good-ldap-changelog"),
+       /** */
+       inetOrgPerson("2.16.840.1.113730.3.2.2", "RFC 2798"),
+       /** */
+       referral("2.16.840.1.113730.3.2.6", "RFC 3296");
+
+       private final static String LDAP_ = "ldap:";
+       private final String oid, spec;
+
+       private LdapObjs(String oid, String spec) {
+               this.oid = oid;
+               this.spec = spec;
+       }
+
+       public String getOid() {
+               return oid;
+       }
+
+       public String getSpec() {
+               return spec;
+       }
+
+       public String property() {
+               return new StringBuilder(LDAP_).append(name()).toString();
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/naming/LdifParser.java b/org.argeo.enterprise/src/org/argeo/naming/LdifParser.java
new file mode 100644 (file)
index 0000000..86392b3
--- /dev/null
@@ -0,0 +1,168 @@
+package org.argeo.naming;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.naming.InvalidNameException;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.osgi.useradmin.UserDirectoryException;
+
+/** Basic LDIF parser. */
+public class LdifParser {
+       private final static Log log = LogFactory.getLog(LdifParser.class);
+       private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+
+       protected Attributes addAttributes(SortedMap<LdapName, Attributes> res, int lineNumber, LdapName currentDn,
+                       Attributes currentAttributes) {
+               try {
+                       Rdn nameRdn = currentDn.getRdn(currentDn.size() - 1);
+                       Attribute nameAttr = currentAttributes.get(nameRdn.getType());
+                       if (nameAttr == null)
+                               currentAttributes.put(nameRdn.getType(), nameRdn.getValue());
+                       else if (!nameAttr.get().equals(nameRdn.getValue()))
+                               throw new UserDirectoryException(
+                                               "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + currentDn
+                                                               + " (shortly before line " + lineNumber + " in LDIF file)");
+                       Attributes previous = res.put(currentDn, currentAttributes);
+                       if (log.isTraceEnabled())
+                               log.trace("Added " + currentDn);
+                       return previous;
+               } catch (NamingException e) {
+                       throw new UserDirectoryException("Cannot add " + currentDn, e);
+               }
+       }
+
+       /** With UTF-8 charset */
+       public SortedMap<LdapName, Attributes> read(InputStream in) throws IOException {
+               try (Reader reader = new InputStreamReader(in, DEFAULT_CHARSET)) {
+                       return read(reader);
+               } finally {
+                       try {
+                               in.close();
+                       } catch (IOException e) {
+                               if (log.isTraceEnabled())
+                                       log.error("Cannot close stream", e);
+                       }
+               }
+       }
+
+       /** 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) {
+                               if (log.isTraceEnabled())
+                                       log.error("Cannot close stream", e);
+                       }
+               }
+               return res;
+       }
+}
\ No newline at end of file
diff --git a/org.argeo.enterprise/src/org/argeo/naming/LdifWriter.java b/org.argeo.enterprise/src/org/argeo/naming/LdifWriter.java
new file mode 100644 (file)
index 0000000..6a3fea1
--- /dev/null
@@ -0,0 +1,78 @@
+package org.argeo.naming;
+
+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 javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+
+import org.argeo.osgi.useradmin.UserDirectoryException;
+
+/** Basic LDIF writer */
+public class LdifWriter {
+       private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+       private final Writer writer;
+
+       /** Writer must be closed by caller */
+       public LdifWriter(Writer writer) {
+               this.writer = writer;
+       }
+
+       /** Stream must be closed by caller */
+       public LdifWriter(OutputStream out) {
+               this(new OutputStreamWriter(out, DEFAULT_CHARSET));
+       }
+
+       public void writeEntry(LdapName name, Attributes attributes) throws IOException {
+               try {
+                       // check consistency
+                       Rdn nameRdn = name.getRdn(name.size() - 1);
+                       Attribute nameAttr = attributes.get(nameRdn.getType());
+                       if (!nameAttr.get().equals(nameRdn.getValue()))
+                               throw new UserDirectoryException(
+                                               "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + name);
+
+                       writer.append(LdapAttrs.DN + ": ").append(name.toString()).append('\n');
+                       Attribute objectClassAttr = attributes.get("objectClass");
+                       if (objectClassAttr != null)
+                               writeAttribute(objectClassAttr);
+                       for (NamingEnumeration<? extends Attribute> attrs = attributes.getAll(); attrs.hasMore();) {
+                               Attribute attribute = attrs.next();
+                               if (attribute.getID().equals(LdapAttrs.DN) || attribute.getID().equals("objectClass"))
+                                       continue;// skip DN attribute
+                               writeAttribute(attribute);
+                       }
+                       writer.append('\n');
+                       writer.flush();
+               } catch (NamingException e) {
+                       throw new UserDirectoryException("Cannot write LDIF", e);
+               }
+       }
+
+       public void write(Map<LdapName, Attributes> entries) throws IOException {
+               for (LdapName dn : entries.keySet())
+                       writeEntry(dn, entries.get(dn));
+       }
+
+       protected void writeAttribute(Attribute attribute) throws NamingException, IOException {
+               for (NamingEnumeration<?> attrValues = attribute.getAll(); attrValues.hasMore();) {
+                       Object value = attrValues.next();
+                       if (value instanceof byte[]) {
+                               String encoded = Base64.getEncoder().encodeToString((byte[]) value);
+                               writer.append(attribute.getID()).append(":: ").append(encoded).append('\n');
+                       } else {
+                               writer.append(attribute.getID()).append(": ").append(value.toString()).append('\n');
+                       }
+               }
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java b/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java
new file mode 100644 (file)
index 0000000..cb93e82
--- /dev/null
@@ -0,0 +1,96 @@
+package org.argeo.naming;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class NamingUtils {
+       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;
+               }
+       }
+
+       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.enterprise/src/org/argeo/naming/SharedSecret.java b/org.argeo.enterprise/src/org/argeo/naming/SharedSecret.java
new file mode 100644 (file)
index 0000000..369b411
--- /dev/null
@@ -0,0 +1,46 @@
+package org.argeo.naming;
+
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+
+public class SharedSecret extends AuthPassword {
+       public final static String X_SHARED_SECRET = "X-SharedSecret";
+       private final Instant expiry;
+
+       public SharedSecret(String authInfo, String authValue) {
+               super(authInfo, authValue);
+               expiry = null;
+       }
+
+       public SharedSecret(AuthPassword authPassword) {
+               super(authPassword);
+               String authInfo = getAuthInfo();
+               if (authInfo.length() == 16) {
+                       expiry = NamingUtils.ldapDateToInstant(authInfo);
+               } else {
+                       expiry = null;
+               }
+       }
+
+       public SharedSecret(ZonedDateTime expiryTimestamp, String value) {
+               super(NamingUtils.instantToLdapDate(expiryTimestamp), value);
+               expiry = expiryTimestamp.withZoneSameInstant(ZoneOffset.UTC).toInstant();
+       }
+
+       public SharedSecret(int hours, String value) {
+               this(ZonedDateTime.now().plusHours(hours), value);
+       }
+
+       @Override
+       protected String getExpectedAuthScheme() {
+               return X_SHARED_SECRET;
+       }
+
+       public boolean isExpired() {
+               if (expiry == null)
+                       return false;
+               return expiry.isBefore(Instant.now());
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/naming/SpecifiedName.java b/org.argeo.enterprise/src/org/argeo/naming/SpecifiedName.java
new file mode 100644 (file)
index 0000000..c59ea2c
--- /dev/null
@@ -0,0 +1,20 @@
+package org.argeo.naming;
+
+/**
+ * A name which has been specified and for which an id has been defined
+ * (typically an OID).
+ */
+public interface SpecifiedName {
+       /** The name */
+       String name();
+
+       /** An RFC or the URLof some specification */
+       default String getSpec() {
+               return null;
+       }
+
+       /** Typicall an OID */
+       default String getID() {
+               return getClass().getName() + "." + name();
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/naming/SrvRecord.java b/org.argeo.enterprise/src/org/argeo/naming/SrvRecord.java
new file mode 100644 (file)
index 0000000..d351588
--- /dev/null
@@ -0,0 +1,52 @@
+package org.argeo.naming;
+
+class SrvRecord implements Comparable<SrvRecord> {
+       private final Integer priority;
+       private final Integer weight;
+       private final Integer port;
+       private final String hostname;
+
+       public SrvRecord(Integer priority, Integer weight, Integer port, String hostname) {
+               this.priority = priority;
+               this.weight = weight;
+               this.port = port;
+               this.hostname = hostname;
+       }
+
+       @Override
+       public int compareTo(SrvRecord other) {
+               // https: // en.wikipedia.org/wiki/SRV_record
+               if (priority != other.priority)
+                       return priority - other.priority;
+               if (weight != other.weight)
+                       return other.weight - other.weight;
+               String host = toHost();
+               String otherHost = other.toHost();
+               if (host.length() == otherHost.length())
+                       return toHost().compareTo(other.toHost());
+               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() {
+               String hostStr = hostname;
+               if (hostname.charAt(hostname.length() - 1) == '.')
+                       hostStr = hostname.substring(0, hostname.length() - 1);
+               return hostStr + ":" + port;
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumAD.java b/org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumAD.java
new file mode 100644 (file)
index 0000000..44b4299
--- /dev/null
@@ -0,0 +1,60 @@
+package org.argeo.osgi.metatype;
+
+import org.argeo.naming.SpecifiedName;
+import org.osgi.service.metatype.AttributeDefinition;
+
+public interface EnumAD extends SpecifiedName, AttributeDefinition {
+       String name();
+
+       default Object getDefault() {
+               return null;
+       }
+
+       @Override
+       default String getName() {
+               return name();
+       }
+
+       @Override
+       default String getID() {
+               return getClass().getName() + "." + name();
+       }
+
+       @Override
+       default String getDescription() {
+               return null;
+       }
+
+       @Override
+       default int getCardinality() {
+               return 0;
+       }
+
+       @Override
+       default int getType() {
+               return STRING;
+       }
+
+       @Override
+       default String[] getOptionValues() {
+               return null;
+       }
+
+       @Override
+       default String[] getOptionLabels() {
+               return null;
+       }
+
+       @Override
+       default String validate(String value) {
+               return null;
+       }
+
+       @Override
+       default String[] getDefaultValue() {
+               Object value = getDefault();
+               if (value == null)
+                       return null;
+               return new String[] { value.toString() };
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumOCD.java b/org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumOCD.java
new file mode 100644 (file)
index 0000000..97c7d56
--- /dev/null
@@ -0,0 +1,54 @@
+package org.argeo.osgi.metatype;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+import org.osgi.service.metatype.AttributeDefinition;
+import org.osgi.service.metatype.ObjectClassDefinition;
+
+public class EnumOCD<T extends Enum<T>> implements ObjectClassDefinition {
+       private final Class<T> enumClass;
+       private String locale;
+
+       public EnumOCD(Class<T> clazz, String locale) {
+               this.enumClass = clazz;
+               this.locale = locale;
+       }
+
+       @Override
+       public String getName() {
+               return null;
+       }
+
+       public String getLocale() {
+               return locale;
+       }
+
+       @Override
+       public String getID() {
+               return enumClass.getName();
+       }
+
+       @Override
+       public String getDescription() {
+               return null;
+       }
+
+       @Override
+       public AttributeDefinition[] getAttributeDefinitions(int filter) {
+               EnumSet<T> set = EnumSet.allOf(enumClass);
+               List<AttributeDefinition> attrs = new ArrayList<>();
+               for (T key : set)
+                       attrs.add((AttributeDefinition) key);
+               return attrs.toArray(new AttributeDefinition[attrs.size()]);
+       }
+
+       @Override
+       public InputStream getIcon(int size) throws IOException {
+               return null;
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java
new file mode 100644 (file)
index 0000000..95b1f07
--- /dev/null
@@ -0,0 +1,509 @@
+package org.argeo.osgi.useradmin;
+
+import static org.argeo.naming.LdapAttrs.objectClass;
+import static org.argeo.naming.LdapObjs.extensibleObject;
+import static org.argeo.naming.LdapObjs.inetOrgPerson;
+import static org.argeo.naming.LdapObjs.organizationalPerson;
+import static org.argeo.naming.LdapObjs.person;
+import static org.argeo.naming.LdapObjs.top;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.naming.InvalidNameException;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import javax.transaction.SystemException;
+import javax.transaction.Transaction;
+import javax.transaction.TransactionManager;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.naming.LdapAttrs;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+
+/** Base class for a {@link UserDirectory}. */
+public abstract class AbstractUserDirectory implements UserAdmin, UserDirectory {
+       static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name";
+       static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password";
+
+       private final static Log log = LogFactory.getLog(AbstractUserDirectory.class);
+
+       private final Hashtable<String, Object> properties;
+       private final LdapName baseDn, userBaseDn, groupBaseDn;
+       private final String userObjectClass, userBase, groupObjectClass, groupBase;
+
+       private final boolean readOnly;
+       private final boolean disabled;
+       private final URI uri;
+
+       private UserAdmin externalRoles;
+       // private List<String> indexedUserProperties = Arrays
+       // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(),
+       // LdapAttrs.cn.name() });
+
+       private String memberAttributeId = "member";
+       private List<String> credentialAttributeIds = Arrays
+                       .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
+
+       // JTA
+       private TransactionManager transactionManager;
+       private WcXaResource xaResource = new WcXaResource(this);
+
+       public AbstractUserDirectory(URI uriArg, Dictionary<String, ?> props) {
+               properties = new Hashtable<String, Object>();
+               for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
+                       String key = keys.nextElement();
+                       properties.put(key, props.get(key));
+               }
+
+               if (uriArg != null) {
+                       uri = uriArg;
+                       // uri from properties is ignored
+               } else {
+                       String uriStr = UserAdminConf.uri.getValue(properties);
+                       if (uriStr == null)
+                               uri = null;
+                       else
+                               try {
+                                       uri = new URI(uriStr);
+                               } catch (URISyntaxException e) {
+                                       throw new UserDirectoryException("Badly formatted URI " + uriStr, e);
+                               }
+               }
+
+               userObjectClass = UserAdminConf.userObjectClass.getValue(properties);
+               userBase = UserAdminConf.userBase.getValue(properties);
+               groupObjectClass = UserAdminConf.groupObjectClass.getValue(properties);
+               groupBase = UserAdminConf.groupBase.getValue(properties);
+               try {
+                       baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
+                       userBaseDn = new LdapName(userBase + "," + baseDn);
+                       groupBaseDn = new LdapName(groupBase + "," + baseDn);
+               } catch (InvalidNameException e) {
+                       throw new UserDirectoryException("Badly formated base DN " + UserAdminConf.baseDn.getValue(properties), e);
+               }
+               String readOnlyStr = UserAdminConf.readOnly.getValue(properties);
+               if (readOnlyStr == null) {
+                       readOnly = readOnlyDefault(uri);
+                       properties.put(UserAdminConf.readOnly.name(), Boolean.toString(readOnly));
+               } else
+                       readOnly = new Boolean(readOnlyStr);
+               String disabledStr = UserAdminConf.disabled.getValue(properties);
+               if (disabledStr != null)
+                       disabled = new Boolean(disabledStr);
+               else
+                       disabled = false;
+       }
+
+       /** Returns the groups this user is a direct member of. */
+       protected abstract List<LdapName> getDirectGroups(LdapName dn);
+
+       protected abstract Boolean daoHasRole(LdapName dn);
+
+       protected abstract DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException;
+
+       protected abstract List<DirectoryUser> doGetRoles(Filter f);
+
+       protected abstract AbstractUserDirectory scope(User user);
+
+       public void init() {
+
+       }
+
+       public void destroy() {
+
+       }
+
+       protected boolean isEditing() {
+               return xaResource.wc() != null;
+       }
+
+       protected UserDirectoryWorkingCopy getWorkingCopy() {
+               UserDirectoryWorkingCopy wc = xaResource.wc();
+               if (wc == null)
+                       return null;
+               return wc;
+       }
+
+       protected void checkEdit() {
+               Transaction transaction;
+               try {
+                       transaction = transactionManager.getTransaction();
+               } catch (SystemException e) {
+                       throw new UserDirectoryException("Cannot get transaction", e);
+               }
+               if (transaction == null)
+                       throw new UserDirectoryException("A transaction needs to be active in order to edit");
+               if (xaResource.wc() == null) {
+                       try {
+                               transaction.enlistResource(xaResource);
+                       } catch (Exception e) {
+                               throw new UserDirectoryException("Cannot enlist " + xaResource, e);
+                       }
+               } else {
+               }
+       }
+
+       protected List<Role> getAllRoles(DirectoryUser user) {
+               List<Role> allRoles = new ArrayList<Role>();
+               if (user != null) {
+                       collectRoles(user, allRoles);
+                       allRoles.add(user);
+               } else
+                       collectAnonymousRoles(allRoles);
+               return allRoles;
+       }
+
+       private void collectRoles(DirectoryUser user, List<Role> allRoles) {
+               Attributes attrs = user.getAttributes();
+               // TODO centralize attribute name
+               Attribute memberOf = attrs.get(LdapAttrs.memberOf.name());
+               if (memberOf != null) {
+                       try {
+                               NamingEnumeration<?> values = memberOf.getAll();
+                               while (values.hasMore()) {
+                                       Object value = values.next();
+                                       LdapName groupDn = new LdapName(value.toString());
+                                       DirectoryUser group = doGetRole(groupDn);
+                                       allRoles.add(group);
+                                       if (log.isTraceEnabled())
+                                               log.trace("Add memberOf " + groupDn);
+                               }
+                       } catch (Exception e) {
+                               throw new UserDirectoryException("Cannot get memberOf groups for " + user, e);
+                       }
+               } else {
+                       for (LdapName groupDn : getDirectGroups(user.getDn())) {
+                               // TODO check for loops
+                               DirectoryUser group = doGetRole(groupDn);
+                               allRoles.add(group);
+                               if (log.isTraceEnabled())
+                                       log.trace("Add direct group " + groupDn);
+                               collectRoles(group, allRoles);
+                       }
+               }
+       }
+
+       private void collectAnonymousRoles(List<Role> allRoles) {
+               // TODO gather anonymous roles
+       }
+
+       // USER ADMIN
+       @Override
+       public Role getRole(String name) {
+               return doGetRole(toDn(name));
+       }
+
+       protected DirectoryUser doGetRole(LdapName dn) {
+               UserDirectoryWorkingCopy wc = getWorkingCopy();
+               DirectoryUser user;
+               try {
+                       user = daoGetRole(dn);
+               } catch (NameNotFoundException e) {
+                       user = null;
+               }
+               if (wc != null) {
+                       if (user == null && wc.getNewUsers().containsKey(dn))
+                               user = wc.getNewUsers().get(dn);
+                       else if (wc.getDeletedUsers().containsKey(dn))
+                               user = null;
+               }
+               return user;
+       }
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public Role[] getRoles(String filter) throws InvalidSyntaxException {
+               UserDirectoryWorkingCopy wc = getWorkingCopy();
+               Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
+               List<DirectoryUser> res = doGetRoles(f);
+               if (wc != null) {
+                       for (Iterator<DirectoryUser> it = res.iterator(); it.hasNext();) {
+                               DirectoryUser user = it.next();
+                               LdapName dn = user.getDn();
+                               if (wc.getDeletedUsers().containsKey(dn))
+                                       it.remove();
+                       }
+                       for (DirectoryUser user : wc.getNewUsers().values()) {
+                               if (f == null || f.match(user.getProperties()))
+                                       res.add(user);
+                       }
+                       // no need to check modified users,
+                       // since doGetRoles was already based on the modified attributes
+               }
+               return res.toArray(new Role[res.size()]);
+       }
+
+       @Override
+       public User getUser(String key, String value) {
+               // TODO check value null or empty
+               List<DirectoryUser> collectedUsers = new ArrayList<DirectoryUser>();
+               if (key != null) {
+                       doGetUser(key, value, collectedUsers);
+               } else {
+                       throw new UserDirectoryException("Key cannot be null");
+                       // // try dn
+                       // DirectoryUser user = null;
+                       // try {
+                       // user = (DirectoryUser) getRole(value);
+                       // if (user != null)
+                       // collectedUsers.add(user);
+                       // } catch (Exception e) {
+                       // // silent
+                       // }
+                       // // try all indexes
+                       // for (String attr : getIndexedUserProperties())
+                       // doGetUser(attr, value, collectedUsers);
+               }
+               if (collectedUsers.size() == 1)
+                       return collectedUsers.get(0);
+               else if (collectedUsers.size() > 1)
+                       log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" : "") + value);
+               return null;
+       }
+
+       protected void doGetUser(String key, String value, List<DirectoryUser> collectedUsers) {
+               try {
+                       Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")");
+                       List<DirectoryUser> users = doGetRoles(f);
+                       collectedUsers.addAll(users);
+               } catch (InvalidSyntaxException e) {
+                       throw new UserDirectoryException("Cannot get user with " + key + "=" + value, e);
+               }
+       }
+
+       @Override
+       public Authorization getAuthorization(User user) {
+               if (user == null || user instanceof DirectoryUser) {
+                       return new LdifAuthorization(user, getAllRoles((DirectoryUser) user));
+               } else {
+                       // bind
+                       AbstractUserDirectory scopedUserAdmin = scope(user);
+                       try {
+                               DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName());
+                               if (directoryUser == null)
+                                       throw new UserDirectoryException("No scoped user found for " + user);
+                               LdifAuthorization authorization = new LdifAuthorization(directoryUser,
+                                               scopedUserAdmin.getAllRoles(directoryUser));
+                               return authorization;
+                       } finally {
+                               scopedUserAdmin.destroy();
+                       }
+               }
+       }
+
+       @Override
+       public Role createRole(String name, int type) {
+               checkEdit();
+               UserDirectoryWorkingCopy wc = getWorkingCopy();
+               LdapName dn = toDn(name);
+               if ((daoHasRole(dn) && !wc.getDeletedUsers().containsKey(dn)) || wc.getNewUsers().containsKey(dn))
+                       throw new UserDirectoryException("Already a role " + name);
+               BasicAttributes attrs = new BasicAttributes(true);
+               // attrs.put(LdifName.dn.name(), dn.toString());
+               Rdn nameRdn = dn.getRdn(dn.size() - 1);
+               // TODO deal with multiple attr RDN
+               attrs.put(nameRdn.getType(), nameRdn.getValue());
+               if (wc.getDeletedUsers().containsKey(dn)) {
+                       wc.getDeletedUsers().remove(dn);
+                       wc.getModifiedUsers().put(dn, attrs);
+                       return getRole(name);
+               } else {
+                       wc.getModifiedUsers().put(dn, attrs);
+                       DirectoryUser newRole = newRole(dn, type, attrs);
+                       wc.getNewUsers().put(dn, newRole);
+                       return newRole;
+               }
+       }
+
+       protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) {
+               LdifUser newRole;
+               BasicAttribute objClass = new BasicAttribute(objectClass.name());
+               if (type == Role.USER) {
+                       String userObjClass = newUserObjectClass(dn);
+                       objClass.add(userObjClass);
+                       if (inetOrgPerson.name().equals(userObjClass)) {
+                               objClass.add(organizationalPerson.name());
+                               objClass.add(person.name());
+                       } else if (organizationalPerson.name().equals(userObjClass)) {
+                               objClass.add(person.name());
+                       }
+                       objClass.add(top.name());
+                       objClass.add(extensibleObject.name());
+                       attrs.put(objClass);
+                       newRole = new LdifUser(this, dn, attrs);
+               } else if (type == Role.GROUP) {
+                       String groupObjClass = getGroupObjectClass();
+                       objClass.add(groupObjClass);
+                       // objClass.add(LdifName.extensibleObject.name());
+                       objClass.add(top.name());
+                       attrs.put(objClass);
+                       newRole = new LdifGroup(this, dn, attrs);
+               } else
+                       throw new UserDirectoryException("Unsupported type " + type);
+               return newRole;
+       }
+
+       @Override
+       public boolean removeRole(String name) {
+               checkEdit();
+               UserDirectoryWorkingCopy wc = getWorkingCopy();
+               LdapName dn = toDn(name);
+               boolean actuallyDeleted;
+               if (daoHasRole(dn) || wc.getNewUsers().containsKey(dn)) {
+                       DirectoryUser user = (DirectoryUser) getRole(name);
+                       wc.getDeletedUsers().put(dn, user);
+                       actuallyDeleted = true;
+               } else {// just removing from groups (e.g. system roles)
+                       actuallyDeleted = false;
+               }
+               for (LdapName groupDn : getDirectGroups(dn)) {
+                       DirectoryUser group = doGetRole(groupDn);
+                       group.getAttributes().get(getMemberAttributeId()).remove(dn.toString());
+               }
+               return actuallyDeleted;
+       }
+
+       // TRANSACTION
+       protected void prepare(UserDirectoryWorkingCopy wc) {
+
+       }
+
+       protected void commit(UserDirectoryWorkingCopy wc) {
+
+       }
+
+       protected void rollback(UserDirectoryWorkingCopy wc) {
+
+       }
+
+       // UTILITIES
+       protected LdapName toDn(String name) {
+               try {
+                       return new LdapName(name);
+               } catch (InvalidNameException e) {
+                       throw new UserDirectoryException("Badly formatted name", e);
+               }
+       }
+
+       // GETTERS
+       protected String getMemberAttributeId() {
+               return memberAttributeId;
+       }
+
+       protected List<String> getCredentialAttributeIds() {
+               return credentialAttributeIds;
+       }
+
+       protected URI getUri() {
+               return uri;
+       }
+
+       private static boolean readOnlyDefault(URI uri) {
+               if (uri == null)
+                       return true;
+               if (uri.getScheme() == null)
+                       return false;// assume relative file to be writable
+               if (uri.getScheme().equals(UserAdminConf.SCHEME_FILE)) {
+                       File file = new File(uri);
+                       if (file.exists())
+                               return !file.canWrite();
+                       else
+                               return !file.getParentFile().canWrite();
+               } else if (uri.getScheme().equals(UserAdminConf.SCHEME_LDAP)) {
+                       if (uri.getAuthority() != null)// assume writable if authenticated
+                               return false;
+               } else if (uri.getScheme().equals(UserAdminConf.SCHEME_OS)) {
+                       return true;
+               }
+               return true;// read only by default
+       }
+
+       public boolean isReadOnly() {
+               return readOnly;
+       }
+
+       public boolean isDisabled() {
+               return disabled;
+       }
+
+       protected UserAdmin getExternalRoles() {
+               return externalRoles;
+       }
+
+       protected int roleType(LdapName dn) {
+               if (dn.startsWith(groupBaseDn))
+                       return Role.GROUP;
+               else if (dn.startsWith(userBaseDn))
+                       return Role.USER;
+               else
+                       return Role.GROUP;
+       }
+
+       /** dn can be null, in that case a default should be returned. */
+       public String getUserObjectClass() {
+               return userObjectClass;
+       }
+
+       public String getUserBase() {
+               return userBase;
+       }
+
+       protected String newUserObjectClass(LdapName dn) {
+               return getUserObjectClass();
+       }
+
+       public String getGroupObjectClass() {
+               return groupObjectClass;
+       }
+
+       public String getGroupBase() {
+               return groupBase;
+       }
+
+       public LdapName getBaseDn() {
+               return (LdapName) baseDn.clone();
+       }
+
+       public Dictionary<String, Object> getProperties() {
+               return properties;
+       }
+
+       public Dictionary<String, Object> cloneProperties() {
+               return new Hashtable<>(properties);
+       }
+
+       public void setExternalRoles(UserAdmin externalRoles) {
+               this.externalRoles = externalRoles;
+       }
+
+       public void setTransactionManager(TransactionManager transactionManager) {
+               this.transactionManager = transactionManager;
+       }
+
+       public WcXaResource getXaResource() {
+               return xaResource;
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java
new file mode 100644 (file)
index 0000000..b450b72
--- /dev/null
@@ -0,0 +1,73 @@
+package org.argeo.osgi.useradmin;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.osgi.service.useradmin.Authorization;
+
+class AggregatingAuthorization implements Authorization {
+       private final String name;
+       private final String displayName;
+       private final List<String> systemRoles;
+       private final List<String> roles;
+
+       public AggregatingAuthorization(String name, String displayName,
+                       Collection<String> systemRoles, String[] roles) {
+               this.name = name;
+               this.displayName = displayName;
+               this.systemRoles = Collections.unmodifiableList(new ArrayList<String>(
+                               systemRoles));
+               this.roles = Collections.unmodifiableList(Arrays.asList(roles));
+       }
+
+       @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.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java
new file mode 100644 (file)
index 0000000..cc1dadb
--- /dev/null
@@ -0,0 +1,221 @@
+package org.argeo.osgi.useradmin;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.LdapAttrs;
+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;
+
+       // DAOs
+       private AbstractUserDirectory systemRoles = null;
+       private Map<LdapName, AbstractUserDirectory> businessRoles = new HashMap<LdapName, AbstractUserDirectory>();
+
+       public AggregatingUserAdmin(String systemRolesBaseDn) {
+               try {
+                       this.systemRolesBaseDn = new LdapName(systemRolesBaseDn);
+               } catch (InvalidNameException e) {
+                       throw new UserDirectoryException("Cannot initialize " + AggregatingUserAdmin.class, e);
+               }
+       }
+
+       @Override
+       public Role createRole(String name, int type) {
+               return findUserAdmin(name).createRole(name, type);
+       }
+
+       @Override
+       public boolean removeRole(String name) {
+               boolean actuallyDeleted = findUserAdmin(name).removeRole(name);
+               systemRoles.removeRole(name);
+               return actuallyDeleted;
+       }
+
+       @Override
+       public Role getRole(String name) {
+               return findUserAdmin(name).getRole(name);
+       }
+
+       @Override
+       public Role[] getRoles(String filter) throws InvalidSyntaxException {
+               List<Role> res = new ArrayList<Role>();
+               for (UserAdmin userAdmin : businessRoles.values()) {
+                       res.addAll(Arrays.asList(userAdmin.getRoles(filter)));
+               }
+               res.addAll(Arrays.asList(systemRoles.getRoles(filter)));
+               return res.toArray(new Role[res.size()]);
+       }
+
+       @Override
+       public User getUser(String key, String value) {
+               List<User> res = new ArrayList<User>();
+               for (UserAdmin userAdmin : businessRoles.values()) {
+                       User u = userAdmin.getUser(key, value);
+                       if (u != null)
+                               res.add(u);
+               }
+               // Note: node roles cannot contain users, so it is not searched
+               return res.size() == 1 ? res.get(0) : null;
+       }
+
+       @Override
+       public Authorization getAuthorization(User user) {
+               if (user == null) {// anonymous
+                       return systemRoles.getAuthorization(null);
+               }
+               UserAdmin userAdmin = findUserAdmin(user.getName());
+               Authorization rawAuthorization = userAdmin.getAuthorization(user);
+               String usernameToUse;
+               String displayNameToUse;
+               if (user instanceof Group) {
+                       String ownerDn = (String) user.getProperties().get(LdapAttrs.owner.name());
+                       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 system roles
+               Set<String> sysRoles = new HashSet<String>();
+               for (String role : rawAuthorization.getRoles()) {
+                       Authorization auth = systemRoles.getAuthorization((User) userAdmin.getRole(role));
+                       sysRoles.addAll(Arrays.asList(auth.getRoles()));
+               }
+               addAbstractSystemRoles(rawAuthorization, sysRoles);
+               Authorization authorization = new AggregatingAuthorization(usernameToUse, displayNameToUse, sysRoles,
+                               rawAuthorization.getRoles());
+               return authorization;
+       }
+
+       /**
+        * Enrich with application-specific roles which are strictly programmatic, such
+        * as anonymous/user semantics.
+        */
+       protected void addAbstractSystemRoles(Authorization rawAuthorization, Set<String> sysRoles) {
+
+       }
+
+       //
+       // USER ADMIN AGGREGATOR
+       //
+       protected void addUserDirectory(AbstractUserDirectory userDirectory) {
+               LdapName baseDn = userDirectory.getBaseDn();
+               if (isSystemRolesBaseDn(baseDn)) {
+                       this.systemRoles = userDirectory;
+                       systemRoles.setExternalRoles(this);
+               } else {
+                       if (businessRoles.containsKey(baseDn))
+                               throw new UserDirectoryException("There is already a user admin for " + baseDn);
+                       businessRoles.put(baseDn, userDirectory);
+               }
+               userDirectory.init();
+               postAdd(userDirectory);
+       }
+
+       /** Called after a new user directory has been added */
+       protected void postAdd(AbstractUserDirectory userDirectory) {
+       }
+
+       private UserAdmin findUserAdmin(String name) {
+               try {
+                       UserAdmin userAdmin = findUserAdmin(new LdapName(name));
+                       return userAdmin;
+               } catch (InvalidNameException e) {
+                       throw new UserDirectoryException("Badly formatted name " + name, e);
+               }
+       }
+
+       private UserAdmin findUserAdmin(LdapName name) {
+               if (name.startsWith(systemRolesBaseDn))
+                       return systemRoles;
+               List<UserAdmin> res = new ArrayList<UserAdmin>(1);
+               for (LdapName baseDn : businessRoles.keySet()) {
+                       if (name.startsWith(baseDn)) {
+                               AbstractUserDirectory ud = businessRoles.get(baseDn);
+                               if (!ud.isDisabled())
+                                       res.add(ud);
+                       }
+               }
+               if (res.size() == 0)
+                       throw new UserDirectoryException("Cannot find user admin for " + name);
+               if (res.size() > 1)
+                       throw new UserDirectoryException("Multiple user admin found for " + name);
+               return res.get(0);
+       }
+
+       protected boolean isSystemRolesBaseDn(LdapName baseDn) {
+               return baseDn.equals(systemRolesBaseDn);
+       }
+
+       protected Dictionary<String, Object> currentState() {
+               Dictionary<String, Object> res = new Hashtable<String, Object>();
+               // res.put(NodeConstants.CN, NodeConstants.DEFAULT);
+               for (LdapName name : businessRoles.keySet()) {
+                       AbstractUserDirectory userDirectory = businessRoles.get(name);
+                       String uri = UserAdminConf.propertiesAsUri(userDirectory.getProperties()).toString();
+                       res.put(uri, "");
+               }
+               return res;
+       }
+
+       public void destroy() {
+               for (LdapName name : businessRoles.keySet()) {
+                       AbstractUserDirectory userDirectory = businessRoles.get(name);
+                       destroy(userDirectory);
+               }
+               businessRoles.clear();
+               businessRoles = null;
+               destroy(systemRoles);
+               systemRoles = null;
+       }
+
+       private void destroy(AbstractUserDirectory userDirectory) {
+               preDestroy(userDirectory);
+               userDirectory.destroy();
+       }
+
+       protected void removeUserDirectory(LdapName baseDn) {
+               if (isSystemRolesBaseDn(baseDn))
+                       throw new UserDirectoryException("System roles cannot be removed ");
+               if (!businessRoles.containsKey(baseDn))
+                       throw new UserDirectoryException("No user directory registered for " + baseDn);
+               AbstractUserDirectory userDirectory = businessRoles.remove(baseDn);
+               destroy(userDirectory);
+       }
+
+       /**
+        * Called before each user directory is destroyed, so that additional actions
+        * can be performed.
+        */
+       protected void preDestroy(AbstractUserDirectory userDirectory) {
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java
new file mode 100644 (file)
index 0000000..6bf1441
--- /dev/null
@@ -0,0 +1,68 @@
+package org.argeo.osgi.useradmin;
+
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import javax.naming.ldap.LdapName;
+
+import org.osgi.service.useradmin.User;
+
+/**
+ * A special user type used during authentication in order to provide the
+ * credentials required for scoping the user admin.
+ */
+public class AuthenticatingUser implements User {
+       /** From com.sun.security.auth.module.*LoginModule */
+       public final static String SHARED_STATE_NAME = "javax.security.auth.login.name";
+       /** From com.sun.security.auth.module.*LoginModule */
+       public final static String SHARED_STATE_PWD = "javax.security.auth.login.password";
+
+       private final String name;
+       private final Dictionary<String, Object> credentials;
+
+       public AuthenticatingUser(LdapName name) {
+               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) {
+               this.name = name;
+               credentials = new Hashtable<>();
+               credentials.put(SHARED_STATE_NAME, name);
+               byte[] pwd = DigestUtils.charsToBytes(password);
+               credentials.put(SHARED_STATE_PWD, pwd);
+       }
+
+       @Override
+       public String getName() {
+               return name;
+       }
+
+       @Override
+       public int getType() {
+               return User.USER;
+       }
+
+       @SuppressWarnings("rawtypes")
+       @Override
+       public Dictionary getProperties() {
+               throw new UnsupportedOperationException();
+       }
+
+       @SuppressWarnings("rawtypes")
+       @Override
+       public Dictionary getCredentials() {
+               return credentials;
+       }
+
+       @Override
+       public boolean hasCredential(String key, Object value) {
+               throw new UnsupportedOperationException();
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java
new file mode 100644 (file)
index 0000000..51d1834
--- /dev/null
@@ -0,0 +1,69 @@
+package org.argeo.osgi.useradmin;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+/** Utilities around digests, mostly those related to passwords. */
+class DigestUtils {
+       static byte[] sha1(byte[] bytes) {
+               try {
+                       MessageDigest digest = MessageDigest.getInstance("SHA1");
+                       digest.update(bytes);
+                       byte[] checksum = digest.digest();
+                       return checksum;
+               } catch (Exception e) {
+                       throw new UserDirectoryException("Cannot SHA1 digest", e);
+               }
+       }
+
+       static char[] bytesToChars(Object obj) {
+               if (obj instanceof char[])
+                       return (char[]) obj;
+               if (!(obj instanceof byte[]))
+                       throw new IllegalArgumentException(obj.getClass() + " is not a byte array");
+               ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj);
+               CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer);
+               char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(), toBuffer.limit());
+               // Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data
+               // Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data
+               // Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data
+               return res;
+       }
+
+       static byte[] charsToBytes(char[] chars) {
+               CharBuffer charBuffer = CharBuffer.wrap(chars);
+               ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
+               byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
+               // Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data
+               // Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
+               return bytes;
+       }
+
+       static String sha1str(String str) {
+               byte[] hash = sha1(str.getBytes(StandardCharsets.UTF_8));
+               return encodeHexString(hash);
+       }
+
+       final private static char[] hexArray = "0123456789abcdef".toCharArray();
+
+       /**
+        * From
+        * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
+        * -a-hex-string-in-java
+        */
+       public static String encodeHexString(byte[] bytes) {
+               char[] hexChars = new char[bytes.length * 2];
+               for (int j = 0; j < bytes.length; j++) {
+                       int v = bytes[j] & 0xFF;
+                       hexChars[j * 2] = hexArray[v >>> 4];
+                       hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+               }
+               return new String(hexChars);
+       }
+
+       private DigestUtils() {
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryGroup.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryGroup.java
new file mode 100644 (file)
index 0000000..7f80463
--- /dev/null
@@ -0,0 +1,12 @@
+package org.argeo.osgi.useradmin;
+
+import java.util.List;
+
+import javax.naming.ldap.LdapName;
+
+import org.osgi.service.useradmin.Group;
+
+/** A group in a user directroy. */
+interface DirectoryGroup extends Group, DirectoryUser {
+       List<LdapName> getMemberNames();
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryUser.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryUser.java
new file mode 100644 (file)
index 0000000..146b805
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.osgi.useradmin;
+
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.osgi.service.useradmin.User;
+
+/** A user in a user directory. */
+interface DirectoryUser extends User {
+       LdapName getDn();
+
+       Attributes getAttributes();
+
+       void publishAttributes(Attributes modifiedAttributes);
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/IpaUtils.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/IpaUtils.java
new file mode 100644 (file)
index 0000000..9d0056c
--- /dev/null
@@ -0,0 +1,54 @@
+package org.argeo.osgi.useradmin;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.LdapAttrs;
+
+/** Free IPA specific conventions. */
+public class IpaUtils {
+       public final static String IPA_USER_BASE = "cn=users,cn=accounts";
+       public final static String IPA_GROUP_BASE = "cn=groups,cn=accounts";
+       public final static String IPA_SERVICE_BASE = "cn=services,cn=accounts";
+
+       private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase();
+
+       public final static String IPA_USER_DIRECTORY_CONFIG = UserAdminConf.userBase + "=" + IPA_USER_BASE + "&"
+                       + UserAdminConf.groupBase + "=" + IPA_GROUP_BASE + "&" + UserAdminConf.readOnly + "=true";
+
+       static String domainToUserDirectoryConfigPath(String realm) {
+               return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + UserAdminConf.realm.name() + "=" + realm;
+       }
+
+       public static String domainToBaseDn(String domain) {
+               String[] dcs = domain.split("\\.");
+               StringBuilder sb = new StringBuilder();
+               for (int i = 0; i < dcs.length; i++) {
+                       if (i != 0)
+                               sb.append(',');
+                       String dc = dcs[i];
+                       sb.append(LdapAttrs.dc.name()).append('=').append(dc.toLowerCase());
+               }
+               return sb.toString();
+       }
+
+       public static LdapName kerberosToDn(String kerberosName) {
+               String[] kname = kerberosName.split("@");
+               String username = kname[0];
+               String baseDn = domainToBaseDn(kname[1]);
+               String dn;
+               if (!username.contains("/"))
+                       dn = LdapAttrs.uid + "=" + username + "," + IPA_USER_BASE + "," + baseDn;
+               else
+                       dn = KRB_PRINCIPAL_NAME + "=" + kerberosName + "," + IPA_SERVICE_BASE + "," + baseDn;
+               try {
+                       return new LdapName(dn);
+               } catch (InvalidNameException e) {
+                       throw new IllegalArgumentException("Badly formatted name for " + kerberosName + ": " + dn);
+               }
+       }
+
+       private IpaUtils() {
+
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java
new file mode 100644 (file)
index 0000000..6dbf6c2
--- /dev/null
@@ -0,0 +1,261 @@
+package org.argeo.osgi.useradmin;
+
+import static org.argeo.naming.LdapAttrs.objectClass;
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.naming.Binding;
+import javax.naming.Context;
+import javax.naming.InvalidNameException;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapName;
+import javax.transaction.TransactionManager;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.naming.LdapAttrs;
+import org.osgi.framework.Filter;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * A user admin based on a LDAP server. Requires a {@link TransactionManager}
+ * and an open transaction for write access.
+ */
+public class LdapUserAdmin extends AbstractUserDirectory {
+       private final static Log log = LogFactory.getLog(LdapUserAdmin.class);
+
+       private InitialLdapContext initialLdapContext = null;
+
+       public LdapUserAdmin(Dictionary<String, ?> properties) {
+               super(null, 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, getUri().toString());
+                       connEnv.put("java.naming.ldap.attributes.binary", LdapAttrs.userPassword.name());
+
+                       initialLdapContext = new InitialLdapContext(connEnv, null);
+                       // StartTlsResponse tls = (StartTlsResponse) ctx
+                       // .extendedOperation(new StartTlsRequest());
+                       // tls.negotiate();
+                       Object securityAuthentication = properties.get(Context.SECURITY_AUTHENTICATION);
+                       if (securityAuthentication != null)
+                               initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, securityAuthentication);
+                       else
+                               initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
+                       Object principal = properties.get(Context.SECURITY_PRINCIPAL);
+                       if (principal != null) {
+                               initialLdapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, principal.toString());
+                               Object creds = properties.get(Context.SECURITY_CREDENTIALS);
+                               if (creds != null) {
+                                       initialLdapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, creds.toString());
+
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new UserDirectoryException("Cannot connect to LDAP", e);
+               }
+       }
+
+       public void destroy() {
+               try {
+                       // tls.close();
+                       initialLdapContext.close();
+               } catch (NamingException e) {
+                       log.error("Cannot destroy LDAP user admin", e);
+               }
+       }
+
+       @SuppressWarnings("unchecked")
+       @Override
+       protected AbstractUserDirectory scope(User user) {
+               Dictionary<String, Object> credentials = user.getCredentials();
+               String username = (String) credentials.get(SHARED_STATE_USERNAME);
+               if (username == null)
+                       username = user.getName();
+               Dictionary<String, Object> properties = cloneProperties();
+               properties.put(Context.SECURITY_PRINCIPAL, username.toString());
+               Object pwdCred = credentials.get(SHARED_STATE_PASSWORD);
+               byte[] pwd = (byte[]) pwdCred;
+               if (pwd != null) {
+                       char[] password = DigestUtils.bytesToChars(pwd);
+                       properties.put(Context.SECURITY_CREDENTIALS, new String(password));
+               } else {
+                       properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
+               }
+               return new LdapUserAdmin(properties);
+       }
+
+       protected InitialLdapContext getLdapContext() {
+               return initialLdapContext;
+       }
+
+       @Override
+       protected Boolean daoHasRole(LdapName dn) {
+               try {
+                       return daoGetRole(dn) != null;
+               } catch (NameNotFoundException e) {
+                       return false;
+               }
+       }
+
+       @Override
+       protected DirectoryUser daoGetRole(LdapName name) throws NameNotFoundException {
+               try {
+                       Attributes attrs = getLdapContext().getAttributes(name);
+                       if (attrs.size() == 0)
+                               return null;
+                       int roleType = roleType(name);
+                       LdifUser res;
+                       if (roleType == Role.GROUP)
+                               res = new LdifGroup(this, name, attrs);
+                       else if (roleType == Role.USER)
+                               res = new LdifUser(this, name, attrs);
+                       else
+                               throw new UserDirectoryException("Unsupported LDAP type for " + name);
+                       return res;
+               } catch (NameNotFoundException e) {
+                       throw e;
+               } catch (NamingException e) {
+                       if (log.isTraceEnabled())
+                               log.error("Cannot get role: " + name, e);
+                       return null;
+               }
+       }
+
+       @Override
+       protected List<DirectoryUser> doGetRoles(Filter f) {
+               try {
+                       String searchFilter = f != null ? f.toString()
+                                       : "(|(" + objectClass + "=" + getUserObjectClass() + ")(" + objectClass + "="
+                                                       + getGroupObjectClass() + "))";
+                       SearchControls searchControls = new SearchControls();
+                       searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+
+                       LdapName searchBase = getBaseDn();
+                       NamingEnumeration<SearchResult> results = getLdapContext().search(searchBase, searchFilter, searchControls);
+
+                       ArrayList<DirectoryUser> res = new ArrayList<DirectoryUser>();
+                       results: while (results.hasMoreElements()) {
+                               SearchResult searchResult = results.next();
+                               Attributes attrs = searchResult.getAttributes();
+                               Attribute objectClassAttr = attrs.get(objectClass.name());
+                               LdapName dn = toDn(searchBase, searchResult);
+                               LdifUser role;
+                               if (objectClassAttr.contains(getGroupObjectClass())
+                                               || objectClassAttr.contains(getGroupObjectClass().toLowerCase()))
+                                       role = new LdifGroup(this, dn, attrs);
+                               else if (objectClassAttr.contains(getUserObjectClass())
+                                               || objectClassAttr.contains(getUserObjectClass().toLowerCase()))
+                                       role = new LdifUser(this, dn, attrs);
+                               else {
+                                       log.warn("Unsupported LDAP type for " + searchResult.getName());
+                                       continue results;
+                               }
+                               res.add(role);
+                       }
+                       return res;
+               } catch (Exception e) {
+                       throw new UserDirectoryException("Cannot get roles for filter " + f, e);
+               }
+       }
+
+       private LdapName toDn(LdapName baseDn, Binding binding) throws InvalidNameException {
+               return new LdapName(binding.isRelative() ? binding.getName() + "," + baseDn : binding.getName());
+       }
+
+       @Override
+       protected List<LdapName> getDirectGroups(LdapName dn) {
+               List<LdapName> directGroups = new ArrayList<LdapName>();
+               try {
+                       String searchFilter = "(&(" + objectClass + "=" + getGroupObjectClass() + ")(" + getMemberAttributeId()
+                                       + "=" + dn + "))";
+
+                       SearchControls searchControls = new SearchControls();
+                       searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+
+                       LdapName searchBase = getBaseDn();
+                       NamingEnumeration<SearchResult> results = getLdapContext().search(searchBase, searchFilter, searchControls);
+
+                       while (results.hasMoreElements()) {
+                               SearchResult searchResult = (SearchResult) results.nextElement();
+                               directGroups.add(toDn(searchBase, searchResult));
+                       }
+                       return directGroups;
+               } catch (Exception e) {
+                       throw new UserDirectoryException("Cannot populate direct members of " + dn, e);
+               }
+       }
+
+       @Override
+       protected void prepare(UserDirectoryWorkingCopy wc) {
+               try {
+                       getLdapContext().reconnect(getLdapContext().getConnectControls());
+                       // delete
+                       for (LdapName dn : wc.getDeletedUsers().keySet()) {
+                               if (!entryExists(dn))
+                                       throw new UserDirectoryException("User to delete no found " + dn);
+                       }
+                       // add
+                       for (LdapName dn : wc.getNewUsers().keySet()) {
+                               if (entryExists(dn))
+                                       throw new UserDirectoryException("User to create found " + dn);
+                       }
+                       // modify
+                       for (LdapName dn : wc.getModifiedUsers().keySet()) {
+                               if (!wc.getNewUsers().containsKey(dn) && !entryExists(dn))
+                                       throw new UserDirectoryException("User to modify not found " + dn);
+                       }
+               } catch (NamingException e) {
+                       throw new UserDirectoryException("Cannot prepare LDAP", e);
+               }
+       }
+
+       private boolean entryExists(LdapName dn) throws NamingException {
+               try {
+                       return getLdapContext().getAttributes(dn).size() != 0;
+               } catch (NameNotFoundException e) {
+                       return false;
+               }
+       }
+
+       @Override
+       protected void commit(UserDirectoryWorkingCopy wc) {
+               try {
+                       // delete
+                       for (LdapName dn : wc.getDeletedUsers().keySet()) {
+                               getLdapContext().destroySubcontext(dn);
+                       }
+                       // add
+                       for (LdapName dn : wc.getNewUsers().keySet()) {
+                               DirectoryUser user = wc.getNewUsers().get(dn);
+                               getLdapContext().createSubcontext(dn, user.getAttributes());
+                       }
+                       // modify
+                       for (LdapName dn : wc.getModifiedUsers().keySet()) {
+                               Attributes modifiedAttrs = wc.getModifiedUsers().get(dn);
+                               getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs);
+                       }
+               } catch (NamingException e) {
+                       throw new UserDirectoryException("Cannot commit LDAP", e);
+               }
+       }
+
+       @Override
+       protected void rollback(UserDirectoryWorkingCopy wc) {
+               // prepare not impacting
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifAuthorization.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifAuthorization.java
new file mode 100644 (file)
index 0000000..354f8c0
--- /dev/null
@@ -0,0 +1,85 @@
+package org.argeo.osgi.useradmin;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.List;
+
+import org.argeo.naming.LdapAttrs;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/** Basic authorization. */
+class LdifAuthorization implements Authorization {
+       private final String name;
+       private final String displayName;
+       private final List<String> allRoles;
+
+       public LdifAuthorization(User user, List<Role> allRoles) {
+               if (user == null) {
+                       this.name = null;
+                       this.displayName = "anonymous";
+               } else {
+                       this.name = user.getName();
+                       this.displayName = extractDisplayName(user);
+               }
+               // roles
+               String[] roles = new String[allRoles.size()];
+               for (int i = 0; i < allRoles.size(); i++) {
+                       roles[i] = allRoles.get(i).getName();
+               }
+               this.allRoles = Collections.unmodifiableList(Arrays.asList(roles));
+       }
+
+       @Override
+       public String getName() {
+               return name;
+       }
+
+       @Override
+       public boolean hasRole(String name) {
+               return allRoles.contains(name);
+       }
+
+       @Override
+       public String[] getRoles() {
+               return allRoles.toArray(new String[allRoles.size()]);
+       }
+
+       @Override
+       public int hashCode() {
+               if (name == null)
+                       return super.hashCode();
+               return name.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (!(obj instanceof Authorization))
+                       return false;
+               Authorization that = (Authorization) obj;
+               if (name == null)
+                       return that.getName() == null;
+               return name.equals(that.getName());
+       }
+
+       @Override
+       public String toString() {
+               return displayName;
+       }
+
+       final static String extractDisplayName(User user) {
+               Dictionary<String, Object> props = user.getProperties();
+               Object displayName = props.get(LdapAttrs.displayName);
+               if (displayName == null)
+                       displayName = props.get(LdapAttrs.cn);
+               if (displayName == null)
+                       displayName = props.get(LdapAttrs.uid);
+               if (displayName == null)
+                       displayName = user.getName();
+               if (displayName == null)
+                       throw new UserDirectoryException("Cannot set display name for " + user);
+               return displayName.toString();
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifGroup.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifGroup.java
new file mode 100644 (file)
index 0000000..bd12911
--- /dev/null
@@ -0,0 +1,106 @@
+package org.argeo.osgi.useradmin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.osgi.service.useradmin.Role;
+
+/** Directory group implementation */
+class LdifGroup extends LdifUser implements DirectoryGroup {
+       private final String memberAttributeId;
+
+       LdifGroup(AbstractUserDirectory userAdmin, LdapName dn,
+                       Attributes attributes) {
+               super(userAdmin, dn, attributes);
+               memberAttributeId = userAdmin.getMemberAttributeId();
+       }
+
+       @Override
+       public boolean addMember(Role role) {
+               getUserAdmin().checkEdit();
+               if (!isEditing())
+                       startEditing();
+
+               Attribute member = getAttributes().get(memberAttributeId);
+               if (member != null) {
+                       if (member.contains(role.getName()))
+                               return false;
+                       else
+                               member.add(role.getName());
+               } else
+                       getAttributes().put(memberAttributeId, role.getName());
+               return true;
+       }
+
+       @Override
+       public boolean addRequiredMember(Role role) {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public boolean removeMember(Role role) {
+               getUserAdmin().checkEdit();
+               if (!isEditing())
+                       startEditing();
+
+               Attribute member = getAttributes().get(memberAttributeId);
+               if (member != null) {
+                       if (!member.contains(role.getName()))
+                               return false;
+                       member.remove(role.getName());
+                       return true;
+               } else
+                       return false;
+       }
+
+       @Override
+       public Role[] getMembers() {
+               List<Role> directMembers = new ArrayList<Role>();
+               for (LdapName ldapName : getMemberNames()) {
+                       Role role = getUserAdmin().getRole(ldapName.toString());
+                       if (role == null) {
+                               if (getUserAdmin().getExternalRoles() != null)
+                                       role = getUserAdmin().getExternalRoles().getRole(
+                                                       ldapName.toString());
+                       }
+                       if (role == null)
+                               throw new UserDirectoryException("No role found for "
+                                               + ldapName);
+                       directMembers.add(role);
+               }
+               return directMembers.toArray(new Role[directMembers.size()]);
+       }
+
+       @Override
+       public List<LdapName> getMemberNames() {
+               Attribute memberAttribute = getAttributes().get(memberAttributeId);
+               if (memberAttribute == null)
+                       return new ArrayList<LdapName>();
+               try {
+                       List<LdapName> roles = new ArrayList<LdapName>();
+                       NamingEnumeration<?> values = memberAttribute.getAll();
+                       while (values.hasMore()) {
+                               LdapName dn = new LdapName(values.next().toString());
+                               roles.add(dn);
+                       }
+                       return roles;
+               } catch (Exception e) {
+                       throw new UserDirectoryException("Cannot get members", e);
+               }
+       }
+
+       @Override
+       public Role[] getRequiredMembers() {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public int getType() {
+               return GROUP;
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java
new file mode 100644 (file)
index 0000000..392b174
--- /dev/null
@@ -0,0 +1,428 @@
+package org.argeo.osgi.useradmin;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.AuthPassword;
+import org.argeo.naming.LdapAttrs;
+import org.argeo.naming.SharedSecret;
+
+/** Directory user implementation */
+class LdifUser implements DirectoryUser {
+       private final AbstractUserDirectory userAdmin;
+
+       private final LdapName dn;
+
+       private final boolean frozen;
+       private Attributes publishedAttributes;
+
+       private final AttributeDictionary properties;
+       private final AttributeDictionary credentials;
+
+       LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) {
+               this(userAdmin, dn, attributes, false);
+       }
+
+       private LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes, boolean frozen) {
+               this.userAdmin = userAdmin;
+               this.dn = dn;
+               this.publishedAttributes = attributes;
+               properties = new AttributeDictionary(false);
+               credentials = new AttributeDictionary(true);
+               this.frozen = frozen;
+       }
+
+       @Override
+       public String getName() {
+               return dn.toString();
+       }
+
+       @Override
+       public int getType() {
+               return USER;
+       }
+
+       @Override
+       public Dictionary<String, Object> getProperties() {
+               return properties;
+       }
+
+       @Override
+       public Dictionary<String, Object> getCredentials() {
+               return credentials;
+       }
+
+       @Override
+       public boolean hasCredential(String key, Object value) {
+               if (key == null) {
+                       // TODO check other sources (like PKCS12)
+                       // String pwd = new String((char[]) value);
+                       // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112)
+                       char[] password = DigestUtils.bytesToChars(value);
+                       AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password);
+                       if (authPassword != null) {
+                               if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) {
+                                       SharedSecret onceToken = new SharedSecret(authPassword);
+                                       if (onceToken.isExpired()) {
+                                               // AuthPassword.remove(getAttributes(), onceToken);
+                                               return false;
+                                       } else {
+                                               // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
+                                               return true;
+                                       }
+                                       // TODO delete expired tokens?
+                               } else {
+                                       // TODO implement SHA
+                                       throw new UnsupportedOperationException(
+                                                       "Unsupported authPassword scheme " + authPassword.getAuthScheme());
+                               }
+                       }
+
+                       // Regular password
+                       byte[] hashedPassword = hash(password);
+                       if (hasCredential(LdapAttrs.userPassword.name(), hashedPassword))
+                               return true;
+                       // if (hasCredential(LdapAttrs.authPassword.name(), pwd))
+                       // return true;
+                       return false;
+               }
+
+               // authPassword (RFC 3112 https://tools.ietf.org/html/rfc3112)
+               // if (key.startsWith(ClientToken.X_CLIENT_TOKEN)) {
+               // return ClientToken.checkAttribute(getAttributes(), key, value);
+               // } else if (key.startsWith(OnceToken.X_ONCE_TOKEN)) {
+               // return OnceToken.checkAttribute(getAttributes(), key, value);
+               // }
+               // StringTokenizer st = new StringTokenizer((String) storedValue, "$ ");
+               // // TODO make it more robust, deal with bad formatting
+               // String authScheme = st.nextToken();
+               // String authInfo = st.nextToken();
+               // String authValue = st.nextToken();
+               // if (authScheme.equals(UriToken.X_URI_TOKEN)) {
+               // UriToken token = new UriToken((String)storedValue);
+               // try {
+               // URI uri = new URI(authInfo);
+               // Map<String, List<String>> query = NamingUtils.queryToMap(uri);
+               // String expiryTimestamp = NamingUtils.getQueryValue(query,
+               // LdapAttrs.modifyTimestamp.name());
+               // if (expiryTimestamp != null) {
+               // Instant expiryOdt = NamingUtils.ldapDateToInstant(expiryTimestamp);
+               // if (expiryOdt.isBefore(Instant.now()))
+               // return false;
+               // } else {
+               // throw new UnsupportedOperationException("An expiry timestamp "
+               // + LdapAttrs.modifyTimestamp.name() + " must be set in the URI query");
+               // }
+               // byte[] hash = Base64.getDecoder().decode(authValue);
+               // byte[] hashedInput = DigestUtils.sha1((authInfo +
+               // value).getBytes(StandardCharsets.US_ASCII));
+               // return Arrays.equals(hash, hashedInput);
+               // } catch (URISyntaxException e) {
+               // throw new UserDirectoryException("Badly formatted " + authInfo, e);
+               // }
+               // }
+
+               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[])
+                       return Arrays.equals((byte[]) storedValue, (byte[]) value);
+               return false;
+       }
+
+       /** Hash and clear the password */
+       private byte[] hash(char[] password) {
+               byte[] hashedPassword = ("{SHA}"
+                               + Base64.getEncoder().encodeToString(DigestUtils.sha1(DigestUtils.charsToBytes(password))))
+                                               .getBytes(StandardCharsets.UTF_8);
+               // Arrays.fill(password, '\u0000');
+               return hashedPassword;
+       }
+
+       // private byte[] toBytes(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;
+       // }
+       //
+       // private char[] toChars(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;
+       // }
+       //
+       @Override
+       public LdapName getDn() {
+               return dn;
+       }
+
+       @Override
+       public synchronized Attributes getAttributes() {
+               return isEditing() ? getModifiedAttributes() : publishedAttributes;
+       }
+
+       /** Should only be called from working copy thread. */
+       private synchronized Attributes getModifiedAttributes() {
+               assert getWc() != null;
+               return getWc().getAttributes(getDn());
+       }
+
+       protected synchronized boolean isEditing() {
+               return getWc() != null && getModifiedAttributes() != null;
+       }
+
+       private synchronized UserDirectoryWorkingCopy getWc() {
+               return userAdmin.getWorkingCopy();
+       }
+
+       protected synchronized void startEditing() {
+               if (frozen)
+                       throw new UserDirectoryException("Cannot edit frozen view");
+               if (getUserAdmin().isReadOnly())
+                       throw new UserDirectoryException("User directory is read-only");
+               assert getModifiedAttributes() == null;
+               getWc().startEditing(this);
+               // modifiedAttributes = (Attributes) publishedAttributes.clone();
+       }
+
+       public synchronized void publishAttributes(Attributes modifiedAttributes) {
+               publishedAttributes = modifiedAttributes;
+       }
+
+       public DirectoryUser getPublished() {
+               return new LdifUser(userAdmin, dn, publishedAttributes, true);
+       }
+
+       @Override
+       public int hashCode() {
+               return dn.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (this == obj)
+                       return true;
+               if (obj instanceof LdifUser) {
+                       LdifUser that = (LdifUser) obj;
+                       return this.dn.equals(that.dn);
+               }
+               return false;
+       }
+
+       @Override
+       public String toString() {
+               return dn.toString();
+       }
+
+       protected AbstractUserDirectory getUserAdmin() {
+               return userAdmin;
+       }
+
+       private class AttributeDictionary extends Dictionary<String, Object> {
+               private final List<String> effectiveKeys = new ArrayList<String>();
+               private final List<String> attrFilter;
+               private final Boolean includeFilter;
+
+               public AttributeDictionary(Boolean includeFilter) {
+                       this.attrFilter = userAdmin.getCredentialAttributeIds();
+                       this.includeFilter = includeFilter;
+                       try {
+                               NamingEnumeration<String> ids = getAttributes().getIDs();
+                               while (ids.hasMore()) {
+                                       String id = ids.next();
+                                       if (includeFilter && attrFilter.contains(id))
+                                               effectiveKeys.add(id);
+                                       else if (!includeFilter && !attrFilter.contains(id))
+                                               effectiveKeys.add(id);
+                               }
+                       } catch (NamingException e) {
+                               throw new UserDirectoryException("Cannot initialise attribute dictionary", e);
+                       }
+               }
+
+               @Override
+               public int size() {
+                       return effectiveKeys.size();
+               }
+
+               @Override
+               public boolean isEmpty() {
+                       return effectiveKeys.size() == 0;
+               }
+
+               @Override
+               public Enumeration<String> keys() {
+                       return Collections.enumeration(effectiveKeys);
+               }
+
+               @Override
+               public Enumeration<Object> elements() {
+                       final Iterator<String> it = effectiveKeys.iterator();
+                       return new Enumeration<Object>() {
+
+                               @Override
+                               public boolean hasMoreElements() {
+                                       return it.hasNext();
+                               }
+
+                               @Override
+                               public Object nextElement() {
+                                       String key = it.next();
+                                       return get(key);
+                               }
+
+                       };
+               }
+
+               @Override
+               public Object get(Object key) {
+                       try {
+                               Attribute attr = getAttributes().get(key.toString());
+                               if (attr == null)
+                                       return null;
+                               Object value = attr.get();
+                               if (value instanceof byte[]) {
+                                       if (key.equals(LdapAttrs.userPassword.name()))
+                                               // TODO other cases (certificates, images)
+                                               return value;
+                                       value = new String((byte[]) value, StandardCharsets.UTF_8);
+                               }
+                               if (attr.size() == 1)
+                                       return value;
+                               if (!attr.getID().equals(LdapAttrs.objectClass.name()))
+                                       return value;
+                               // special case for object class
+                               NamingEnumeration<?> en = attr.getAll();
+                               Set<String> objectClasses = new HashSet<String>();
+                               while (en.hasMore()) {
+                                       String objectClass = en.next().toString();
+                                       objectClasses.add(objectClass);
+                               }
+
+                               if (objectClasses.contains(userAdmin.getUserObjectClass()))
+                                       return userAdmin.getUserObjectClass();
+                               else if (objectClasses.contains(userAdmin.getGroupObjectClass()))
+                                       return userAdmin.getGroupObjectClass();
+                               else
+                                       return value;
+                       } catch (NamingException e) {
+                               throw new UserDirectoryException("Cannot get value for attribute " + key, e);
+                       }
+               }
+
+               @Override
+               public Object put(String key, Object value) {
+                       if (key == null) {
+                               // TODO persist to other sources (like PKCS12)
+                               char[] password = DigestUtils.bytesToChars(value);
+                               byte[] hashedPassword = hash(password);
+                               return put(LdapAttrs.userPassword.name(), hashedPassword);
+                       }
+                       if (key.startsWith("X-")) {
+                               return put(LdapAttrs.authPassword.name(), value);
+                       }
+
+                       userAdmin.checkEdit();
+                       if (!isEditing())
+                               startEditing();
+
+                       if (!(value instanceof String || value instanceof byte[]))
+                               throw new IllegalArgumentException("Value must be String or byte[]");
+
+                       if (includeFilter && !attrFilter.contains(key))
+                               throw new IllegalArgumentException("Key " + key + " not included");
+                       else if (!includeFilter && attrFilter.contains(key))
+                               throw new IllegalArgumentException("Key " + key + " excluded");
+
+                       try {
+                               Attribute attribute = getModifiedAttributes().get(key.toString());
+                               // if (attribute == null) // block unit tests
+                               attribute = new BasicAttribute(key.toString());
+                               if (value instanceof String && !isAsciiPrintable(((String) value)))
+                                       attribute.add(((String) value).getBytes(StandardCharsets.UTF_8));
+                               else
+                                       attribute.add(value);
+                               Attribute previousAttribute = getModifiedAttributes().put(attribute);
+                               if (previousAttribute != null)
+                                       return previousAttribute.get();
+                               else
+                                       return null;
+                       } catch (NamingException e) {
+                               throw new UserDirectoryException("Cannot get value for attribute " + key, e);
+                       }
+               }
+
+               @Override
+               public Object remove(Object key) {
+                       userAdmin.checkEdit();
+                       if (!isEditing())
+                               startEditing();
+
+                       if (includeFilter && !attrFilter.contains(key))
+                               throw new IllegalArgumentException("Key " + key + " not included");
+                       else if (!includeFilter && attrFilter.contains(key))
+                               throw new IllegalArgumentException("Key " + key + " excluded");
+
+                       try {
+                               Attribute attr = getModifiedAttributes().remove(key.toString());
+                               if (attr != null)
+                                       return attr.get();
+                               else
+                                       return null;
+                       } catch (NamingException e) {
+                               throw new UserDirectoryException("Cannot remove attribute " + key, e);
+                       }
+               }
+       }
+
+       private static boolean isAsciiPrintable(String str) {
+               if (str == null) {
+                       return false;
+               }
+               int sz = str.length();
+               for (int i = 0; i < sz; i++) {
+                       if (isAsciiPrintable(str.charAt(i)) == false) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+
+       private static boolean isAsciiPrintable(char ch) {
+               return ch >= 32 && ch < 127;
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUserAdmin.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUserAdmin.java
new file mode 100644 (file)
index 0000000..8668152
--- /dev/null
@@ -0,0 +1,259 @@
+package org.argeo.osgi.useradmin;
+
+import static org.argeo.naming.LdapAttrs.objectClass;
+import static org.argeo.naming.LdapObjs.inetOrgPerson;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+import javax.transaction.TransactionManager;
+
+import org.argeo.naming.LdifParser;
+import org.argeo.naming.LdifWriter;
+import org.osgi.framework.Filter;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * A user admin based on a LDIF files. Requires a {@link TransactionManager} and
+ * an open transaction for write access.
+ */
+public class LdifUserAdmin extends AbstractUserDirectory {
+       private SortedMap<LdapName, DirectoryUser> users = new TreeMap<LdapName, DirectoryUser>();
+       private SortedMap<LdapName, DirectoryGroup> groups = new TreeMap<LdapName, DirectoryGroup>();
+
+       public LdifUserAdmin(String uri, String baseDn) {
+               this(fromUri(uri, baseDn));
+       }
+
+       public LdifUserAdmin(Dictionary<String, ?> properties) {
+               super(null, properties);
+       }
+
+       public LdifUserAdmin(URI uri, Dictionary<String, ?> properties) {
+               super(uri, properties);
+       }
+
+       @SuppressWarnings("unchecked")
+       @Override
+       protected AbstractUserDirectory scope(User user) {
+               Dictionary<String, Object> credentials = user.getCredentials();
+               String username = (String) credentials.get(SHARED_STATE_USERNAME);
+               if (username == null)
+                       username = user.getName();
+               Object pwdCred = credentials.get(SHARED_STATE_PASSWORD);
+               byte[] pwd = (byte[]) pwdCred;
+               if (pwd != null) {
+                       char[] password = DigestUtils.bytesToChars(pwd);
+                       User directoryUser = (User) getRole(username);
+                       if (!directoryUser.hasCredential(null, password))
+                               throw new UserDirectoryException("Invalid credentials");
+               } else {
+                       throw new UserDirectoryException("Password is required");
+               }
+               Dictionary<String, Object> properties = cloneProperties();
+               properties.put(UserAdminConf.readOnly.name(), "true");
+               LdifUserAdmin scopedUserAdmin = new LdifUserAdmin(properties);
+               scopedUserAdmin.groups = Collections.unmodifiableSortedMap(groups);
+               scopedUserAdmin.users = Collections.unmodifiableSortedMap(users);
+               return scopedUserAdmin;
+       }
+
+       private static Dictionary<String, Object> fromUri(String uri, String baseDn) {
+               Hashtable<String, Object> res = new Hashtable<String, Object>();
+               res.put(UserAdminConf.uri.name(), uri);
+               res.put(UserAdminConf.baseDn.name(), baseDn);
+               return res;
+       }
+
+       public void init() {
+               try {
+                       if (getUri().getScheme().equals("file")) {
+                               File file = new File(getUri());
+                               if (!file.exists())
+                                       return;
+                       }
+                       load(getUri().toURL().openStream());
+               } catch (Exception e) {
+                       throw new UserDirectoryException("Cannot open URL " + getUri(), e);
+               }
+       }
+
+       public void save() {
+               if (getUri() == null)
+                       throw new UserDirectoryException("Cannot save LDIF user admin: no URI is set");
+               if (isReadOnly())
+                       throw new UserDirectoryException("Cannot save LDIF user admin: " + getUri() + " is read-only");
+               try (FileOutputStream out = new FileOutputStream(new File(getUri()))) {
+                       save(out);
+               } catch (IOException e) {
+                       throw new UserDirectoryException("Cannot save user admin to " + getUri(), e);
+               }
+       }
+
+       public void save(OutputStream out) throws IOException {
+               try {
+                       LdifWriter ldifWriter = new LdifWriter(out);
+                       for (LdapName name : groups.keySet())
+                               ldifWriter.writeEntry(name, groups.get(name).getAttributes());
+                       for (LdapName name : users.keySet())
+                               ldifWriter.writeEntry(name, users.get(name).getAttributes());
+               } finally {
+                       out.close();
+               }
+       }
+
+       protected void load(InputStream in) {
+               try {
+                       users.clear();
+                       groups.clear();
+
+                       LdifParser ldifParser = new LdifParser();
+                       SortedMap<LdapName, Attributes> allEntries = ldifParser.read(in);
+                       for (LdapName key : allEntries.keySet()) {
+                               Attributes attributes = allEntries.get(key);
+                               // check for inconsistency
+                               Set<String> lowerCase = new HashSet<String>();
+                               NamingEnumeration<String> ids = attributes.getIDs();
+                               while (ids.hasMoreElements()) {
+                                       String id = ids.nextElement().toLowerCase();
+                                       if (lowerCase.contains(id))
+                                               throw new UserDirectoryException(key + " has duplicate id " + id);
+                                       lowerCase.add(id);
+                               }
+
+                               // analyse object classes
+                               NamingEnumeration<?> objectClasses = attributes.get(objectClass.name()).getAll();
+                               // System.out.println(key);
+                               objectClasses: while (objectClasses.hasMore()) {
+                                       String objectClass = objectClasses.next().toString();
+                                       // System.out.println(" " + objectClass);
+                                       if (objectClass.equals(inetOrgPerson.name())) {
+                                               users.put(key, new LdifUser(this, key, attributes));
+                                               break objectClasses;
+                                       } else if (objectClass.equals(getGroupObjectClass())) {
+                                               groups.put(key, new LdifGroup(this, key, attributes));
+                                               break objectClasses;
+                                       }
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new UserDirectoryException("Cannot load user admin service from LDIF", e);
+               }
+       }
+
+       public void destroy() {
+               if (users == null || groups == null)
+                       throw new UserDirectoryException("User directory " + getBaseDn() + " is already destroyed");
+               users = null;
+               groups = null;
+       }
+
+       @Override
+       protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException {
+               if (groups.containsKey(key))
+                       return groups.get(key);
+               if (users.containsKey(key))
+                       return users.get(key);
+               throw new NameNotFoundException(key + " not persisted");
+       }
+
+       @Override
+       protected Boolean daoHasRole(LdapName dn) {
+               return users.containsKey(dn) || groups.containsKey(dn);
+       }
+
+       @SuppressWarnings("unchecked")
+       protected List<DirectoryUser> doGetRoles(Filter f) {
+               ArrayList<DirectoryUser> res = new ArrayList<DirectoryUser>();
+               if (f == null) {
+                       res.addAll(users.values());
+                       res.addAll(groups.values());
+               } else {
+                       for (DirectoryUser user : users.values()) {
+                               if (f.match(user.getProperties()))
+                                       res.add(user);
+                       }
+                       for (DirectoryUser group : groups.values())
+                               if (f.match(group.getProperties()))
+                                       res.add(group);
+               }
+               return res;
+       }
+
+       @Override
+       protected List<LdapName> getDirectGroups(LdapName dn) {
+               List<LdapName> directGroups = new ArrayList<LdapName>();
+               for (LdapName name : groups.keySet()) {
+                       DirectoryGroup group = groups.get(name);
+                       if (group.getMemberNames().contains(dn))
+                               directGroups.add(group.getDn());
+               }
+               return directGroups;
+       }
+
+       @Override
+       protected void prepare(UserDirectoryWorkingCopy wc) {
+               // delete
+               for (LdapName dn : wc.getDeletedUsers().keySet()) {
+                       if (users.containsKey(dn))
+                               users.remove(dn);
+                       else if (groups.containsKey(dn))
+                               groups.remove(dn);
+                       else
+                               throw new UserDirectoryException("User to delete not found " + dn);
+               }
+               // add
+               for (LdapName dn : wc.getNewUsers().keySet()) {
+                       DirectoryUser user = wc.getNewUsers().get(dn);
+                       if (users.containsKey(dn) || groups.containsKey(dn))
+                               throw new UserDirectoryException("User to create found " + dn);
+                       else if (Role.USER == user.getType())
+                               users.put(dn, user);
+                       else if (Role.GROUP == user.getType())
+                               groups.put(dn, (DirectoryGroup) user);
+                       else
+                               throw new UserDirectoryException("Unsupported role type " + user.getType() + " for new user " + dn);
+               }
+               // modify
+               for (LdapName dn : wc.getModifiedUsers().keySet()) {
+                       Attributes modifiedAttrs = wc.getModifiedUsers().get(dn);
+                       DirectoryUser user;
+                       if (users.containsKey(dn))
+                               user = users.get(dn);
+                       else if (groups.containsKey(dn))
+                               user = groups.get(dn);
+                       else
+                               throw new UserDirectoryException("User to modify no found " + dn);
+                       user.publishAttributes(modifiedAttrs);
+               }
+       }
+
+       @Override
+       protected void commit(UserDirectoryWorkingCopy wc) {
+               save();
+       }
+
+       @Override
+       protected void rollback(UserDirectoryWorkingCopy wc) {
+               init();
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserDirectory.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserDirectory.java
new file mode 100644 (file)
index 0000000..fd78263
--- /dev/null
@@ -0,0 +1,66 @@
+package org.argeo.osgi.useradmin;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.List;
+
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.naming.LdapAttrs;
+import org.osgi.framework.Filter;
+import org.osgi.service.useradmin.User;
+
+public class OsUserDirectory extends AbstractUserDirectory {
+       private final String osUsername = System.getProperty("user.name");
+       private final LdapName osUserDn;
+       private final LdifUser osUser;
+
+       public OsUserDirectory(URI uriArg, Dictionary<String, ?> props) {
+               super(uriArg, props);
+               try {
+                       osUserDn = new LdapName(LdapAttrs.uid.name() + "=" + osUsername + "," + getUserBase() + "," + getBaseDn());
+                       Attributes attributes = new BasicAttributes();
+                       attributes.put(LdapAttrs.uid.name(), osUsername);
+                       osUser = new LdifUser(this, osUserDn, attributes);
+               } catch (NamingException e) {
+                       throw new UserDirectoryException("Cannot create system user", e);
+               }
+       }
+
+       @Override
+       protected List<LdapName> getDirectGroups(LdapName dn) {
+               return new ArrayList<>();
+       }
+
+       @Override
+       protected Boolean daoHasRole(LdapName dn) {
+               return osUserDn.equals(dn);
+       }
+
+       @Override
+       protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException {
+               if (osUserDn.equals(key))
+                       return osUser;
+               else
+                       throw new NameNotFoundException("Not an OS role");
+       }
+
+       @Override
+       protected List<DirectoryUser> doGetRoles(Filter f) {
+               List<DirectoryUser> res = new ArrayList<>();
+               if (f==null || f.match(osUser.getProperties()))
+                       res.add(osUser);
+               return res;
+       }
+
+       @Override
+       protected AbstractUserDirectory scope(User user) {
+               throw new UnsupportedOperationException();
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserUtils.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserUtils.java
new file mode 100644 (file)
index 0000000..ad6bf88
--- /dev/null
@@ -0,0 +1,53 @@
+package org.argeo.osgi.useradmin;
+
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.NoSuchAlgorithmException;
+import java.security.URIParameter;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.Configuration;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+public class OsUserUtils {
+       private static String LOGIN_CONTEXT_USER_NIX = "USER_NIX";
+       private static String LOGIN_CONTEXT_USER_NT = "USER_NT";
+
+       public static String getOsUsername() {
+               return System.getProperty("user.name");
+       }
+
+       public static LoginContext loginAsSystemUser(Subject subject) {
+               try {
+                       URL jaasConfigurationUrl = OsUserUtils.class.getClassLoader()
+                                       .getResource("org/argeo/osgi/useradmin/jaas-os.cfg");
+                       URIParameter uriParameter = new URIParameter(jaasConfigurationUrl.toURI());
+                       Configuration jaasConfiguration = Configuration.getInstance("JavaLoginConfig", uriParameter);
+                       LoginContext lc = new LoginContext(isWindows() ? LOGIN_CONTEXT_USER_NT : LOGIN_CONTEXT_USER_NIX, subject,
+                                       null, jaasConfiguration);
+                       lc.login();
+                       return lc;
+               } catch (URISyntaxException | NoSuchAlgorithmException | LoginException e) {
+                       throw new RuntimeException("Cannot login as system user", e);
+               }
+       }
+
+       public static void main(String args[]) {
+               Subject subject = new Subject();
+               LoginContext loginContext = loginAsSystemUser(subject);
+               System.out.println(subject);
+               try {
+                       loginContext.logout();
+               } catch (LoginException e) {
+                       // silent
+               }
+       }
+
+       private static boolean isWindows() {
+               return System.getProperty("os.name").startsWith("Windows");
+       }
+
+       private OsUserUtils() {
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java
new file mode 100644 (file)
index 0000000..37d6339
--- /dev/null
@@ -0,0 +1,275 @@
+package org.argeo.osgi.useradmin;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapName;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.naming.DnsBrowser;
+import org.argeo.naming.NamingUtils;
+import org.osgi.framework.Constants;
+
+/** Properties used to configure user admins. */
+public enum UserAdminConf {
+       /** Base DN (cannot be configured externally) */
+       baseDn("dc=example,dc=com"),
+
+       /** URI of the underlying resource (cannot be configured externally) */
+       uri("ldap://localhost:10389"),
+
+       /** User objectClass */
+       userObjectClass("inetOrgPerson"),
+
+       /** Relative base DN for users */
+       userBase("ou=People"),
+
+       /** Groups objectClass */
+       groupObjectClass("groupOfNames"),
+
+       /** Relative base DN for users */
+       groupBase("ou=Groups"),
+
+       /** Read-only source */
+       readOnly(null),
+
+       /** Disabled source */
+       disabled(null),
+
+       /** Authentication realm */
+       realm(null);
+
+       public final static String FACTORY_PID = "org.argeo.osgi.useradmin.config";
+       private final static Log log = LogFactory.getLog(UserAdminConf.class);
+
+       public final static String SCHEME_LDAP = "ldap";
+       public final static String SCHEME_FILE = "file";
+       public final static String SCHEME_OS = "os";
+       public final static String SCHEME_IPA = "ipa";
+
+       /** The default value. */
+       private Object def;
+
+       UserAdminConf(Object def) {
+               this.def = def;
+       }
+
+       public Object getDefault() {
+               return def;
+       }
+
+       /**
+        * For use as Java property.
+        * 
+        * @deprecated use {@link #name()} instead
+        */
+       @Deprecated
+       public String property() {
+               return name();
+       }
+
+       public String getValue(Dictionary<String, ?> properties) {
+               Object res = getRawValue(properties);
+               if (res == null)
+                       return null;
+               return res.toString();
+       }
+
+       @SuppressWarnings("unchecked")
+       public <T> T getRawValue(Dictionary<String, ?> properties) {
+               Object res = properties.get(name());
+               if (res == null)
+                       res = getDefault();
+               return (T) res;
+       }
+
+       /** @deprecated use {@link #valueOf(String)} instead */
+       @Deprecated
+       public static UserAdminConf local(String property) {
+               return UserAdminConf.valueOf(property);
+       }
+
+       /** Hides host and credentials. */
+       public static URI propertiesAsUri(Dictionary<String, ?> properties) {
+               StringBuilder query = new StringBuilder();
+
+               boolean first = true;
+               for (Enumeration<String> keys = properties.keys(); keys.hasMoreElements();) {
+                       String key = keys.nextElement();
+                       // TODO clarify which keys are relevant (list only the enum?)
+                       if (!key.equals("service.factoryPid") && !key.equals("cn") && !key.equals("dn")
+                                       && !key.equals(Constants.SERVICE_PID) && !key.startsWith("java") && !key.equals(baseDn.name())
+                                       && !key.equals(uri.name())) {
+                               if (first)
+                                       first = false;
+                               else
+                                       query.append('&');
+                               query.append(valueOf(key).name());
+                               query.append('=').append(properties.get(key).toString());
+                       }
+               }
+
+               String bDn = (String) properties.get(baseDn.name());
+               try {
+                       return new URI(null, null, bDn != null ? '/' + bDn : null, query.length() != 0 ? query.toString() : null,
+                                       null);
+               } catch (URISyntaxException e) {
+                       throw new UserDirectoryException("Cannot create URI from properties", e);
+               }
+       }
+
+       public static Dictionary<String, Object> uriAsProperties(String uriStr) {
+               try {
+                       Hashtable<String, Object> res = new Hashtable<String, Object>();
+                       URI u = new URI(uriStr);
+                       String scheme = u.getScheme();
+                       if (scheme != null && scheme.equals(SCHEME_IPA)) {
+                               u = convertIpaConfig(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("ldaps")) {
+                                       // TODO additional checks
+                                       if (u.getUserInfo() != null) {
+                                               String[] userInfo = u.getUserInfo().split(":");
+                                               principal = userInfo.length > 0 ? userInfo[0] : null;
+                                               credentials = userInfo.length > 1 ? userInfo[1] : null;
+                                       }
+                               } else if (scheme.equals(SCHEME_FILE)) {
+                               } else if (scheme.equals(SCHEME_IPA)) {
+                               } else if (scheme.equals(SCHEME_OS)) {
+                               } else
+                                       throw new UserDirectoryException("Unsupported scheme " + scheme);
+                       Map<String, List<String>> query = NamingUtils.queryToMap(u);
+                       for (String key : query.keySet()) {
+                               UserAdminConf ldapProp = UserAdminConf.valueOf(key);
+                               List<String> values = query.get(key);
+                               if (values.size() == 1) {
+                                       res.put(ldapProp.name(), values.get(0));
+                               } else {
+                                       throw new UserDirectoryException("Only single values are supported");
+                               }
+                       }
+                       res.put(baseDn.name(), bDn);
+                       if (SCHEME_OS.equals(scheme))
+                               res.put(readOnly.name(), "true");
+                       if (principal != null)
+                               res.put(Context.SECURITY_PRINCIPAL, principal);
+                       if (credentials != null)
+                               res.put(Context.SECURITY_CREDENTIALS, credentials);
+                       if (scheme != null) {// relative URIs are dealt with externally
+                               if (SCHEME_OS.equals(scheme)) {
+                                       res.put(uri.name(), SCHEME_OS + ":///");
+                               } else {
+                                       URI bareUri = new URI(scheme, null, u.getHost(), u.getPort(),
+                                                       scheme.equals(SCHEME_FILE) ? u.getPath() : null, null, null);
+                                       res.put(uri.name(), bareUri.toString());
+                               }
+                       }
+                       return res;
+               } catch (Exception e) {
+                       throw new UserDirectoryException("Cannot convert " + uri + " to properties", e);
+               }
+       }
+
+       private static URI convertIpaConfig(URI uri) {
+               String path = uri.getPath();
+               String kerberosRealm;
+               if (path == null || path.length() <= 1) {
+                       kerberosRealm = kerberosDomainFromDns();
+               } else {
+                       kerberosRealm = path.substring(1);
+               }
+
+               if (kerberosRealm == null)
+                       throw new UserDirectoryException("No Kerberos domain available for " + uri);
+               try (DnsBrowser dnsBrowser = new DnsBrowser()) {
+                       String ldapHostsStr = uri.getHost();
+                       if (ldapHostsStr == null || ldapHostsStr.trim().equals("")) {
+                               List<String> ldapHosts = dnsBrowser.getSrvRecordsAsHosts("_ldap._tcp." + kerberosRealm.toLowerCase());
+                               if (ldapHosts == null || ldapHosts.size() == 0) {
+                                       throw new UserDirectoryException("Cannot configure LDAP for IPA " + uri);
+                               } else {
+                                       ldapHostsStr = ldapHosts.get(0);
+                               }
+                       }
+                       URI convertedUri = new URI(
+                                       SCHEME_LDAP + "://" + ldapHostsStr + "/" + IpaUtils.domainToUserDirectoryConfigPath(kerberosRealm));
+                       if (log.isDebugEnabled())
+                               log.debug("Converted " + uri + " to " + convertedUri);
+                       return convertedUri;
+               } catch (NamingException | IOException | URISyntaxException e) {
+                       throw new UserDirectoryException("cannot convert IPA uri " + uri, e);
+               }
+       }
+
+       private static String kerberosDomainFromDns() {
+               String kerberosDomain;
+               try (DnsBrowser dnsBrowser = new DnsBrowser()) {
+                       InetAddress localhost = InetAddress.getLocalHost();
+                       String hostname = localhost.getHostName();
+                       String dnsZone = hostname.substring(hostname.indexOf('.') + 1);
+                       kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT");
+                       return kerberosDomain;
+               } catch (Exception e) {
+                       throw new UserDirectoryException("Cannot determine Kerberos domain from DNS", e);
+               }
+
+       }
+
+       private static String getBaseDnFromHostname() {
+               String hostname;
+               try {
+                       hostname = InetAddress.getLocalHost().getHostName();
+               } catch (UnknownHostException e) {
+                       log.warn("Using localhost as hostname", e);
+                       hostname = "localhost.localdomain";
+               }
+               int dotIdx = hostname.indexOf('.');
+               if (dotIdx >= 0) {
+                       String domain = hostname.substring(dotIdx + 1, hostname.length());
+                       String bDn = ("." + domain).replaceAll("\\.", ",dc=");
+                       bDn = bDn.substring(1, bDn.length());
+                       return bDn;
+               } else {
+                       return "dc=" + hostname;
+               }
+       }
+
+       /**
+        * Hash the base DN in order to have a deterministic string to be used as a cn
+        * for the underlying user directory.
+        */
+       public static String baseDnHash(Dictionary<String, Object> properties) {
+               String bDn = (String) properties.get(baseDn.name());
+               if (bDn == null)
+                       throw new UserDirectoryException("No baseDn in " + properties);
+               return DigestUtils.sha1str(bDn);
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectory.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectory.java
new file mode 100644 (file)
index 0000000..ff80c5a
--- /dev/null
@@ -0,0 +1,25 @@
+package org.argeo.osgi.useradmin;
+
+import javax.naming.ldap.LdapName;
+import javax.transaction.xa.XAResource;
+
+/** Information about a user directory. */
+public interface UserDirectory {
+       /** The base DN of all entries in this user directory */
+       LdapName getBaseDn();
+
+       /** The related {@link XAResource} */
+       XAResource getXaResource();
+
+       boolean isReadOnly();
+
+       boolean isDisabled();
+
+       String getUserObjectClass();
+
+       String getUserBase();
+
+       String getGroupObjectClass();
+
+       String getGroupBase();
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryException.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryException.java
new file mode 100644 (file)
index 0000000..613d0fd
--- /dev/null
@@ -0,0 +1,19 @@
+package org.argeo.osgi.useradmin;
+
+import org.osgi.service.useradmin.UserAdmin;
+
+/**
+ * Exceptions related to Argeo's implementation of OSGi {@link UserAdmin}
+ * service.
+ */
+public class UserDirectoryException extends RuntimeException {
+       private static final long serialVersionUID = 1419352360062048603L;
+
+       public UserDirectoryException(String message) {
+               super(message);
+       }
+
+       public UserDirectoryException(String message, Throwable e) {
+               super(message, e);
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryWorkingCopy.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryWorkingCopy.java
new file mode 100644 (file)
index 0000000..0e25bdf
--- /dev/null
@@ -0,0 +1,58 @@
+package org.argeo.osgi.useradmin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+import javax.transaction.xa.XAResource;
+
+/** {@link XAResource} for a user directory being edited. */
+class UserDirectoryWorkingCopy {
+       // private final static Log log = LogFactory
+       // .getLog(UserDirectoryWorkingCopy.class);
+
+       private Map<LdapName, DirectoryUser> newUsers = new HashMap<LdapName, DirectoryUser>();
+       private Map<LdapName, Attributes> modifiedUsers = new HashMap<LdapName, Attributes>();
+       private Map<LdapName, DirectoryUser> deletedUsers = new HashMap<LdapName, DirectoryUser>();
+
+       void cleanUp() {
+               // clean collections
+               newUsers.clear();
+               newUsers = null;
+               modifiedUsers.clear();
+               modifiedUsers = null;
+               deletedUsers.clear();
+               deletedUsers = null;
+       }
+
+       public boolean noModifications() {
+               return newUsers.size() == 0 && modifiedUsers.size() == 0
+                               && deletedUsers.size() == 0;
+       }
+
+       public Attributes getAttributes(LdapName dn) {
+               if (modifiedUsers.containsKey(dn))
+                       return modifiedUsers.get(dn);
+               return null;
+       }
+
+       public void startEditing(DirectoryUser user) {
+               LdapName dn = user.getDn();
+               if (modifiedUsers.containsKey(dn))
+                       throw new UserDirectoryException("Already editing " + dn);
+               modifiedUsers.put(dn, (Attributes) user.getAttributes().clone());
+       }
+
+       public Map<LdapName, DirectoryUser> getNewUsers() {
+               return newUsers;
+       }
+
+       public Map<LdapName, DirectoryUser> getDeletedUsers() {
+               return deletedUsers;
+       }
+
+       public Map<LdapName, Attributes> getModifiedUsers() {
+               return modifiedUsers;
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/WcXaResource.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/WcXaResource.java
new file mode 100644 (file)
index 0000000..a6048fd
--- /dev/null
@@ -0,0 +1,143 @@
+package org.argeo.osgi.useradmin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.transaction.xa.XAException;
+import javax.transaction.xa.XAResource;
+import javax.transaction.xa.Xid;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/** {@link XAResource} for a user directory being edited. */
+class WcXaResource implements XAResource {
+       private final static Log log = LogFactory.getLog(WcXaResource.class);
+
+       private final AbstractUserDirectory userDirectory;
+
+       private Map<Xid, UserDirectoryWorkingCopy> workingCopies = new HashMap<Xid, UserDirectoryWorkingCopy>();
+       private Xid editingXid = null;
+       private int transactionTimeout = 0;
+
+       public WcXaResource(AbstractUserDirectory userDirectory) {
+               this.userDirectory = userDirectory;
+       }
+
+       @Override
+       public synchronized void start(Xid xid, int flags) throws XAException {
+               if (editingXid != null)
+                       throw new UserDirectoryException("Already editing " + editingXid);
+               UserDirectoryWorkingCopy wc = workingCopies.put(xid,
+                               new UserDirectoryWorkingCopy());
+               if (wc != null)
+                       throw new UserDirectoryException(
+                                       "There is already a working copy for " + xid);
+               this.editingXid = xid;
+       }
+
+       @Override
+       public void end(Xid xid, int flags) throws XAException {
+               checkXid(xid);
+       }
+
+       private UserDirectoryWorkingCopy wc(Xid xid) {
+               return workingCopies.get(xid);
+       }
+
+       synchronized UserDirectoryWorkingCopy wc() {
+               if (editingXid == null)
+                       return null;
+               UserDirectoryWorkingCopy wc = workingCopies.get(editingXid);
+               if (wc == null)
+                       throw new UserDirectoryException("No working copy found for "
+                                       + editingXid);
+               return wc;
+       }
+
+       private synchronized void cleanUp(Xid xid) {
+               wc(xid).cleanUp();
+               workingCopies.remove(xid);
+               editingXid = null;
+       }
+
+       @Override
+       public int prepare(Xid xid) throws XAException {
+               checkXid(xid);
+               UserDirectoryWorkingCopy wc = wc(xid);
+               if (wc.noModifications())
+                       return XA_RDONLY;
+               try {
+                       userDirectory.prepare(wc);
+               } catch (Exception e) {
+                       log.error("Cannot prepare " + xid, e);
+                       throw new XAException(XAException.XAER_RMERR);
+               }
+               return XA_OK;
+       }
+
+       @Override
+       public void commit(Xid xid, boolean onePhase) throws XAException {
+               try {
+                       checkXid(xid);
+                       UserDirectoryWorkingCopy wc = wc(xid);
+                       if (wc.noModifications())
+                               return;
+                       if (onePhase)
+                               userDirectory.prepare(wc);
+                       userDirectory.commit(wc);
+               } catch (Exception e) {
+                       log.error("Cannot commit " + xid, e);
+                       throw new XAException(XAException.XAER_RMERR);
+               } finally {
+                       cleanUp(xid);
+               }
+       }
+
+       @Override
+       public void rollback(Xid xid) throws XAException {
+               try {
+                       checkXid(xid);
+                       userDirectory.rollback(wc(xid));
+               } catch (Exception e) {
+                       log.error("Cannot rollback " + xid, e);
+                       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.enterprise/src/org/argeo/osgi/useradmin/jaas-os.cfg b/org.argeo.enterprise/src/org/argeo/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.enterprise/src/org/argeo/transaction/simple/SimpleTransaction.java b/org.argeo.enterprise/src/org/argeo/transaction/simple/SimpleTransaction.java
new file mode 100644 (file)
index 0000000..fbb1386
--- /dev/null
@@ -0,0 +1,164 @@
+package org.argeo.transaction.simple;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.transaction.HeuristicMixedException;
+import javax.transaction.HeuristicRollbackException;
+import javax.transaction.RollbackException;
+import javax.transaction.Status;
+import javax.transaction.Synchronization;
+import javax.transaction.SystemException;
+import javax.transaction.Transaction;
+import javax.transaction.xa.XAException;
+import javax.transaction.xa.XAResource;
+import javax.transaction.xa.Xid;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+class SimpleTransaction implements Transaction, Status {
+       private final static Log log = LogFactory.getLog(SimpleTransaction.class);
+
+       private final Xid xid;
+       private int status = Status.STATUS_ACTIVE;
+       private final List<XAResource> xaResources = new ArrayList<XAResource>();
+
+       private final SimpleTransactionManager transactionManager;
+
+       public SimpleTransaction(SimpleTransactionManager transactionManager) {
+               this.xid = new UuidXid();
+               this.transactionManager = transactionManager;
+       }
+
+       @Override
+       public synchronized void commit() throws RollbackException,
+                       HeuristicMixedException, HeuristicRollbackException,
+                       SecurityException, IllegalStateException, SystemException {
+               status = STATUS_PREPARING;
+               for (XAResource xaRes : xaResources) {
+                       if (status == STATUS_MARKED_ROLLBACK)
+                               break;
+                       try {
+                               xaRes.prepare(xid);
+                       } catch (XAException e) {
+                               status = STATUS_MARKED_ROLLBACK;
+                               log.error("Cannot prepare " + xaRes + " for " + xid, e);
+                       }
+               }
+               if (status == STATUS_MARKED_ROLLBACK) {
+                       rollback();
+                       throw new RollbackException();
+               }
+               status = STATUS_PREPARED;
+
+               status = STATUS_COMMITTING;
+               for (XAResource xaRes : xaResources) {
+                       if (status == STATUS_MARKED_ROLLBACK)
+                               break;
+                       try {
+                               xaRes.commit(xid, false);
+                       } catch (XAException e) {
+                               status = STATUS_MARKED_ROLLBACK;
+                               log.error("Cannot prepare " + xaRes + " for " + xid, e);
+                       }
+               }
+               if (status == STATUS_MARKED_ROLLBACK) {
+                       rollback();
+                       throw new RollbackException();
+               }
+
+               // complete
+               status = STATUS_COMMITTED;
+               if (log.isTraceEnabled())
+                       log.trace("COMMITTED  " + xid);
+               clearResources(XAResource.TMSUCCESS);
+               transactionManager.unregister(xid);
+       }
+
+       @Override
+       public synchronized void rollback() throws IllegalStateException,
+                       SystemException {
+               status = STATUS_ROLLING_BACK;
+               for (XAResource xaRes : xaResources) {
+                       try {
+                               xaRes.rollback(xid);
+                       } catch (XAException e) {
+                               log.error("Cannot rollback " + xaRes + " for " + xid, e);
+                       }
+               }
+
+               // complete
+               status = STATUS_ROLLEDBACK;
+               if (log.isTraceEnabled())
+                       log.trace("ROLLEDBACK " + xid);
+               clearResources(XAResource.TMFAIL);
+               transactionManager.unregister(xid);
+       }
+
+       @Override
+       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) {
+                               log.error("Cannot enlist " + xaRes, e);
+                               return false;
+                       }
+               } else
+                       return false;
+       }
+
+       @Override
+       public synchronized boolean delistResource(XAResource xaRes, int flag)
+                       throws IllegalStateException, SystemException {
+               if (xaResources.remove(xaRes)) {
+                       try {
+                               xaRes.end(getXid(), flag);
+                       } catch (XAException e) {
+                               log.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) {
+                               log.error("Cannot end " + xaRes, e);
+                       }
+               xaResources.clear();
+       }
+
+       @Override
+       public synchronized int getStatus() throws SystemException {
+               return status;
+       }
+
+       @Override
+       public void registerSynchronization(Synchronization sync)
+                       throws RollbackException, IllegalStateException, SystemException {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public void setRollbackOnly() throws IllegalStateException, SystemException {
+               status = STATUS_MARKED_ROLLBACK;
+       }
+
+       @Override
+       public int hashCode() {
+               return xid.hashCode();
+       }
+
+       Xid getXid() {
+               return xid;
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/transaction/simple/SimpleTransactionException.java b/org.argeo.enterprise/src/org/argeo/transaction/simple/SimpleTransactionException.java
new file mode 100644 (file)
index 0000000..d00def9
--- /dev/null
@@ -0,0 +1,14 @@
+package org.argeo.transaction.simple;
+
+public class SimpleTransactionException extends RuntimeException {
+       private static final long serialVersionUID = -7465792070797750212L;
+
+       public SimpleTransactionException(String message) {
+               super(message);
+       }
+
+       public SimpleTransactionException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/transaction/simple/SimpleTransactionManager.java b/org.argeo.enterprise/src/org/argeo/transaction/simple/SimpleTransactionManager.java
new file mode 100644 (file)
index 0000000..fef7281
--- /dev/null
@@ -0,0 +1,173 @@
+package org.argeo.transaction.simple;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.transaction.HeuristicMixedException;
+import javax.transaction.HeuristicRollbackException;
+import javax.transaction.InvalidTransactionException;
+import javax.transaction.NotSupportedException;
+import javax.transaction.RollbackException;
+import javax.transaction.Status;
+import javax.transaction.Synchronization;
+import javax.transaction.SystemException;
+import javax.transaction.Transaction;
+import javax.transaction.TransactionManager;
+import javax.transaction.TransactionSynchronizationRegistry;
+import javax.transaction.UserTransaction;
+import javax.transaction.xa.Xid;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class SimpleTransactionManager implements TransactionManager, UserTransaction {
+       private final static Log log = LogFactory.getLog(SimpleTransactionManager.class);
+
+       private ThreadLocal<SimpleTransaction> current = new ThreadLocal<SimpleTransaction>();
+
+       private Map<Xid, SimpleTransaction> knownTransactions = Collections
+                       .synchronizedMap(new HashMap<Xid, SimpleTransaction>());
+       private SyncRegistry syncRegistry = new SyncRegistry();
+
+       @Override
+       public void begin() throws NotSupportedException, SystemException {
+               if (getCurrent() != null)
+                       throw new NotSupportedException("Nested transactions are not supported");
+               SimpleTransaction transaction = new SimpleTransaction(this);
+               knownTransactions.put(transaction.getXid(), transaction);
+               current.set(transaction);
+               if (log.isTraceEnabled())
+                       log.trace("STARTED    " + transaction.getXid());
+       }
+
+       @Override
+       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();
+       }
+
+       @Override
+       public int getStatus() throws SystemException {
+               if (getCurrent() == null)
+                       return Status.STATUS_NO_TRANSACTION;
+               return getTransaction().getStatus();
+       }
+
+       @Override
+       public Transaction getTransaction() throws SystemException {
+               return getCurrent();
+       }
+
+       protected SimpleTransaction getCurrent() throws SystemException {
+               SimpleTransaction transaction = current.get();
+               if (transaction == null)
+                       return null;
+               int status = transaction.getStatus();
+               if (Status.STATUS_COMMITTED == status || Status.STATUS_ROLLEDBACK == status) {
+                       current.remove();
+                       return null;
+               }
+               return transaction;
+       }
+
+       void unregister(Xid xid) {
+               knownTransactions.remove(xid);
+       }
+
+       @Override
+       public void resume(Transaction tobj) throws InvalidTransactionException, IllegalStateException, SystemException {
+               if (getCurrent() != null)
+                       throw new IllegalStateException("Transaction " + current.get() + " already registered");
+               current.set((SimpleTransaction) tobj);
+       }
+
+       @Override
+       public void rollback() throws IllegalStateException, SecurityException, SystemException {
+               if (getCurrent() == null)
+                       throw new IllegalStateException("No transaction registered with the current thread.");
+               getCurrent().rollback();
+       }
+
+       @Override
+       public void setRollbackOnly() throws IllegalStateException, SystemException {
+               if (getCurrent() == null)
+                       throw new IllegalStateException("No transaction registered with the current thread.");
+               getCurrent().setRollbackOnly();
+       }
+
+       @Override
+       public void setTransactionTimeout(int seconds) throws SystemException {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public Transaction suspend() throws SystemException {
+               Transaction 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 SimpleTransactionException("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 SimpleTransactionException("Cannot get status", e);
+                       }
+               }
+
+               @Override
+               public boolean getRollbackOnly() {
+                       try {
+                               return getStatus() == Status.STATUS_MARKED_ROLLBACK;
+                       } catch (SystemException e) {
+                               throw new SimpleTransactionException("Cannot get status", e);
+                       }
+               }
+
+               @Override
+               public void setRollbackOnly() {
+                       try {
+                               getCurrent().setRollbackOnly();
+                       } catch (Exception e) {
+                               throw new SimpleTransactionException("Cannot set rollback only", e);
+                       }
+               }
+
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/transaction/simple/UuidXid.java b/org.argeo.enterprise/src/org/argeo/transaction/simple/UuidXid.java
new file mode 100644 (file)
index 0000000..1009c82
--- /dev/null
@@ -0,0 +1,132 @@
+package org.argeo.transaction.simple;
+
+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.ext.jackrabbit/.classpath b/org.argeo.ext.jackrabbit/.classpath
new file mode 100644 (file)
index 0000000..a8a298a
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="src" path="ext/test"/>
+       <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-1.8"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.ext.jackrabbit/.gitignore b/org.argeo.ext.jackrabbit/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.ext.jackrabbit/.project b/org.argeo.ext.jackrabbit/.project
new file mode 100644 (file)
index 0000000..fde35cc
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.ext.jackrabbit</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.ext.jackrabbit/META-INF/.gitignore b/org.argeo.ext.jackrabbit/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.ext.jackrabbit/bnd.bnd b/org.argeo.ext.jackrabbit/bnd.bnd
new file mode 100644 (file)
index 0000000..8107e87
--- /dev/null
@@ -0,0 +1,4 @@
+Fragment-Host: org.apache.jackrabbit.core
+Import-Package: org.springframework.core,\
+org.argeo.node,\
+*
diff --git a/org.argeo.ext.jackrabbit/build.properties b/org.argeo.ext.jackrabbit/build.properties
new file mode 100644 (file)
index 0000000..8cc3392
--- /dev/null
@@ -0,0 +1,23 @@
+source.. = src/,\
+           ext/test/
+
+additional.bundles = org.junit,\
+                     org.apache.jackrabbit.core,\
+                     javax.jcr,\
+                     org.apache.jackrabbit.api,\
+                     org.apache.jackrabbit.data,\
+                     org.apache.jackrabbit.jcr.commons,\
+                     org.apache.jackrabbit.spi,\
+                     org.apache.jackrabbit.spi.commons,\
+                     org.slf4j.api,\
+                     org.slf4j.commons.logging,\
+                     org.slf4j.log4j12,\
+                     org.apache.log4j,\
+                     org.apache.commons.collections,\
+                     EDU.oswego.cs.dl.util.concurrent,\
+                     org.apache.lucene,\
+                     org.apache.tika.core,\
+                     org.apache.commons.dbcp,\
+                     org.apache.commons.pool,\
+                     org.argeo.jcr
+
diff --git a/org.argeo.ext.jackrabbit/ext/test/log4j.properties b/org.argeo.ext.jackrabbit/ext/test/log4j.properties
new file mode 100644 (file)
index 0000000..b4edd7c
--- /dev/null
@@ -0,0 +1,17 @@
+log4j.rootLogger=WARN, console
+
+## Levels
+log4j.logger.org.argeo=DEBUG
+log4j.logger.org.apache.jackrabbit=OFF
+log4j.logger.org.apache.jackrabbit.core.security=DEBUG
+log4j.logger.org.apache.jackrabbit.core.DefaultSecurityManager=DEBUG
+
+## Appenders
+# console is set to be a ConsoleAppender.
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+
+# console uses PatternLayout.
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+#log4j.appender.console.layout.ConversionPattern= %-5p %d{ISO8601} %m - %c%n
+#log4j.appender.console.layout.ConversionPattern=%m%n
+log4j.appender.console.layout.ConversionPattern=%d{ABSOLUTE} %m (%F:%L) [%t] %p %n
diff --git a/org.argeo.ext.jackrabbit/ext/test/org/argeo/security/jackrabbit/JackrabbitAuthTest.java b/org.argeo.ext.jackrabbit/ext/test/org/argeo/security/jackrabbit/JackrabbitAuthTest.java
new file mode 100644 (file)
index 0000000..cc56bd0
--- /dev/null
@@ -0,0 +1,55 @@
+package org.argeo.security.jackrabbit;
+
+import javax.jcr.Repository;
+import javax.jcr.Session;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.jackrabbit.unit.AbstractJackrabbitTestCase;
+
+public class JackrabbitAuthTest extends AbstractJackrabbitTestCase {
+       private final Log log = LogFactory.getLog(JackrabbitAuthTest.class);
+
+       public void testLogin() throws Exception {
+               // FIXME properly log in
+               if(true)
+                       return;
+               Session session = session();
+               log.debug(session.getUserID());
+               assertEquals("admin", session.getUserID());
+               // Subject subject = new Subject();
+               // LoginContext loginContext = new LoginContext("SYSTEM", subject);
+               // loginContext.login();
+               // Subject.doAs(subject, new PrivilegedExceptionAction<Void>() {
+               //
+               // @Override
+               // public Void run() throws Exception {
+               // Repository repository = getRepository();
+               // Session session = repository.login();
+               // log.debug(session.getUserID());
+               // return null;
+               // }
+               // });
+       }
+
+       @Override
+       protected String getLoginContext() {
+               return LOGIN_CONTEXT_TEST_SYSTEM;
+       }
+
+       @Override
+       protected Repository createRepository() throws Exception {
+               return super.createRepository();
+       }
+
+       @Override
+       protected void clearRepository(Repository repository) throws Exception {
+               // System.setProperty("java.security.auth.login.config", "");
+       }
+
+       @Override
+       protected String getRepositoryConfigResource() {
+               return "/org/argeo/security/jackrabbit/repository-memory-test.xml";
+       }
+
+}
diff --git a/org.argeo.ext.jackrabbit/ext/test/org/argeo/security/jackrabbit/repository-memory-test.xml b/org.argeo.ext.jackrabbit/ext/test/org/argeo/security/jackrabbit/repository-memory-test.xml
new file mode 100644 (file)
index 0000000..e285555
--- /dev/null
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<!--
+
+    Copyright (C) 2007-2012 Argeo GmbH
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
+                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="main" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${rep.home}/repository/index" />
+                       <param name="directoryManagerClass"
+                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+                       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/repository/index" />
+               <param name="directoryManagerClass"
+                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security"/>
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager"/>
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.ext.jackrabbit/pom.xml b/org.argeo.ext.jackrabbit/pom.xml
new file mode 100644 (file)
index 0000000..dcb8d82
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.ext.jackrabbit</artifactId>
+       <name>Extension Jackrabbit Core</name>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.node.api</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- TESTING -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.jcr</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <scope>test</scope>
+               </dependency>
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java b/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java
new file mode 100644 (file)
index 0000000..cd0cf86
--- /dev/null
@@ -0,0 +1,22 @@
+package org.argeo.security.jackrabbit;
+
+import java.util.Map;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.jackrabbit.core.security.authorization.acl.ACLProvider;
+
+/** Argeo specific access control provider */
+public class ArgeoAccessControlProvider extends ACLProvider {
+
+       @SuppressWarnings({ "rawtypes", "unchecked" })
+       @Override
+       public void init(Session systemSession, Map configuration)
+                       throws RepositoryException {
+               if (!configuration.containsKey(PARAM_ALLOW_UNKNOWN_PRINCIPALS))
+                       configuration.put(PARAM_ALLOW_UNKNOWN_PRINCIPALS, "true");
+               super.init(systemSession, configuration);
+       }
+
+}
diff --git a/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoAccessManager.java b/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoAccessManager.java
new file mode 100644 (file)
index 0000000..52ea3c9
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.security.jackrabbit;
+
+import javax.jcr.PathNotFoundException;
+import javax.jcr.RepositoryException;
+import javax.jcr.security.Privilege;
+
+import org.apache.jackrabbit.core.id.ItemId;
+import org.apache.jackrabbit.core.security.DefaultAccessManager;
+import org.apache.jackrabbit.spi.Path;
+
+/**
+ * Intermediary class in order to have a consistent naming in config files. Does
+ * nothing for the time being, but may in the future.
+ */
+public class ArgeoAccessManager extends DefaultAccessManager {
+
+       @Override
+       public boolean canRead(Path itemPath, ItemId itemId)
+                       throws RepositoryException {
+               return super.canRead(itemPath, itemId);
+       }
+
+       @Override
+       public Privilege[] getPrivileges(String absPath)
+                       throws PathNotFoundException, RepositoryException {
+               return super.getPrivileges(absPath);
+       }
+
+       @Override
+       public boolean hasPrivileges(String absPath, Privilege[] privileges)
+                       throws PathNotFoundException, RepositoryException {
+               return super.hasPrivileges(absPath, privileges);
+       }
+
+}
diff --git a/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java b/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java
new file mode 100644 (file)
index 0000000..15199c0
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.security.jackrabbit;
+
+import java.security.Principal;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.core.DefaultSecurityManager;
+import org.apache.jackrabbit.core.security.AMContext;
+import org.apache.jackrabbit.core.security.AccessManager;
+import org.apache.jackrabbit.core.security.SecurityConstants;
+import org.apache.jackrabbit.core.security.SystemPrincipal;
+import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager;
+import org.apache.jackrabbit.core.security.principal.AdminPrincipal;
+import org.argeo.node.NodeConstants;
+import org.argeo.node.security.AnonymousPrincipal;
+import org.argeo.node.security.DataAdminPrincipal;
+
+/** Customises Jackrabbit security. */
+public class ArgeoSecurityManager extends DefaultSecurityManager {
+       @Override
+       public AccessManager getAccessManager(Session session, AMContext amContext) throws RepositoryException {
+               synchronized (getSystemSession()) {
+                       return super.getAccessManager(session, amContext);
+               }
+       }
+
+       @Override
+       public UserManager getUserManager(Session session) throws RepositoryException {
+               synchronized (getSystemSession()) {
+                       return super.getUserManager(session);
+               }
+       }
+
+       /** Called once when the session is created */
+       @Override
+       public String getUserID(Subject subject, String workspaceName) throws RepositoryException {
+               boolean isAnonymous = !subject.getPrincipals(AnonymousPrincipal.class).isEmpty();
+               boolean isDataAdmin = !subject.getPrincipals(DataAdminPrincipal.class).isEmpty();
+               boolean isJackrabbitSystem = !subject.getPrincipals(SystemPrincipal.class).isEmpty();
+               Set<X500Principal> userPrincipal = subject.getPrincipals(X500Principal.class);
+               boolean isRegularUser = !userPrincipal.isEmpty();
+               if (isAnonymous) {
+                       if (isDataAdmin || isJackrabbitSystem || isRegularUser)
+                               throw new IllegalStateException("Inconsistent " + subject);
+                       else
+                               return NodeConstants.ROLE_ANONYMOUS;
+               } else if (isRegularUser) {// must be before DataAdmin
+                       if (isAnonymous || isJackrabbitSystem)
+                               throw new IllegalStateException("Inconsistent " + subject);
+                       else {
+                               if (userPrincipal.size() > 1) {
+                                       StringBuilder buf = new StringBuilder();
+                                       for (X500Principal principal : userPrincipal)
+                                               buf.append(' ').append('\"').append(principal).append('\"');
+                                       throw new RuntimeException("Multiple user principals:" + buf);
+                               }
+                               return userPrincipal.iterator().next().getName();
+                       }
+               } else if (isDataAdmin) {
+                       if (isAnonymous || isJackrabbitSystem || isRegularUser)
+                               throw new IllegalStateException("Inconsistent " + subject);
+                       else {
+                               assert !subject.getPrincipals(AdminPrincipal.class).isEmpty();
+                               return NodeConstants.ROLE_DATA_ADMIN;
+                       }
+               } else if (isJackrabbitSystem) {
+                       if (isAnonymous || isDataAdmin || isRegularUser)
+                               throw new IllegalStateException("Inconsistent " + subject);
+                       else
+                               return super.getUserID(subject, workspaceName);
+               } else {
+                       throw new IllegalStateException("Unrecognized subject type: " + subject);
+               }
+       }
+
+       @Override
+       protected WorkspaceAccessManager createDefaultWorkspaceAccessManager() {
+               WorkspaceAccessManager wam = super.createDefaultWorkspaceAccessManager();
+               return new ArgeoWorkspaceAccessManagerImpl(wam);
+       }
+
+       private class ArgeoWorkspaceAccessManagerImpl implements SecurityConstants, WorkspaceAccessManager {
+               private final WorkspaceAccessManager wam;
+
+               public ArgeoWorkspaceAccessManagerImpl(WorkspaceAccessManager wam) {
+                       super();
+                       this.wam = wam;
+               }
+
+               public void init(Session systemSession) throws RepositoryException {
+                       wam.init(systemSession);
+               }
+
+               public void close() throws RepositoryException {
+               }
+
+               public boolean grants(Set<Principal> principals, String workspaceName) throws RepositoryException {
+                       // TODO: implements finer access to workspaces
+                       return true;
+               }
+       }
+
+}
diff --git a/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/SystemJackrabbitLoginModule.java b/org.argeo.ext.jackrabbit/src/org/argeo/security/jackrabbit/SystemJackrabbitLoginModule.java
new file mode 100644 (file)
index 0000000..f7de8d0
--- /dev/null
@@ -0,0 +1,65 @@
+package org.argeo.security.jackrabbit;
+
+import java.util.Map;
+import java.util.Set;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+import javax.security.auth.x500.X500Principal;
+
+import org.apache.jackrabbit.core.security.AnonymousPrincipal;
+import org.apache.jackrabbit.core.security.SecurityConstants;
+import org.apache.jackrabbit.core.security.principal.AdminPrincipal;
+import org.argeo.node.security.DataAdminPrincipal;
+
+public class SystemJackrabbitLoginModule implements LoginModule {
+       private Subject subject;
+
+       @Override
+       public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
+                       Map<String, ?> options) {
+               this.subject = subject;
+       }
+
+       @Override
+       public boolean login() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean commit() throws LoginException {
+               Set<org.argeo.node.security.AnonymousPrincipal> anonPrincipal = subject.getPrincipals(org.argeo.node.security.AnonymousPrincipal.class);
+               if (!anonPrincipal.isEmpty()) {
+                       subject.getPrincipals().add(new AnonymousPrincipal());
+                       return true;
+               }
+
+               Set<DataAdminPrincipal> initPrincipal = subject.getPrincipals(DataAdminPrincipal.class);
+               if (!initPrincipal.isEmpty()) {
+                       subject.getPrincipals().add(new AdminPrincipal(SecurityConstants.ADMIN_ID));
+                       return true;
+               }
+
+               Set<X500Principal> userPrincipal = subject.getPrincipals(X500Principal.class);
+               if (userPrincipal.isEmpty())
+                       throw new LoginException("Subject must be pre-authenticated");
+               if (userPrincipal.size() > 1)
+                       throw new LoginException("Multiple user principals " + userPrincipal);
+
+               return true;
+       }
+
+       @Override
+       public boolean abort() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean logout() throws LoginException {
+               subject.getPrincipals().removeAll(subject.getPrincipals(AnonymousPrincipal.class));
+               subject.getPrincipals().removeAll(subject.getPrincipals(AdminPrincipal.class));
+               return true;
+       }
+}
diff --git a/org.argeo.ext.rap.ui.workbench/.gitignore b/org.argeo.ext.rap.ui.workbench/.gitignore
new file mode 100644 (file)
index 0000000..b83d222
--- /dev/null
@@ -0,0 +1 @@
+/target/
diff --git a/org.argeo.ext.rap.ui.workbench/.project b/org.argeo.ext.rap.ui.workbench/.project
new file mode 100644 (file)
index 0000000..2795baf
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.ext.rap.ui.workbench</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.pde.ManifestBuilder</name>
+                       <arguments />
+               </buildCommand>
+               <buildCommand>
+                       <name>org.eclipse.pde.SchemaBuilder</name>
+                       <arguments />
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.pde.PluginNature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.ext.rap.ui.workbench/META-INF/.gitignore b/org.argeo.ext.rap.ui.workbench/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.ext.rap.ui.workbench/META-INF/spring/osgi.xml b/org.argeo.ext.rap.ui.workbench/META-INF/spring/osgi.xml
new file mode 100644 (file)
index 0000000..206a72a
--- /dev/null
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<beans:beans xmlns="http://www.springframework.org/schema/osgi"\r
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"\r
+       xmlns:util="http://www.springframework.org/schema/util"\r
+       xsi:schemaLocation="http://www.springframework.org/schema/osgi  \r
+       http://www.springframework.org/schema/osgi/spring-osgi-1.1.xsd\r
+       http://www.springframework.org/schema/beans   \r
+       http://www.springframework.org/schema/beans/spring-beans-2.5.xsd\r
+       http://www.springframework.org/schema/util\r
+       http://www.springframework.org/schema/util/spring-util-2.5.xsd">\r
+\r
+       <service interface="org.eclipse.rap.rwt.application.ApplicationConfiguration">\r
+               <service-properties>\r
+                       <beans:entry key="contextName" value="ui" />\r
+               </service-properties>\r
+               <beans:bean\r
+                       class="org.eclipse.rap.ui.internal.servlet.WorkbenchApplicationConfiguration" />\r
+       </service>\r
+</beans:beans>
\ No newline at end of file
diff --git a/org.argeo.ext.rap.ui.workbench/bnd.bnd b/org.argeo.ext.rap.ui.workbench/bnd.bnd
new file mode 100644 (file)
index 0000000..48c602a
--- /dev/null
@@ -0,0 +1,4 @@
+Bundle-SymbolicName: org.argeo.ext.rap.ui.workbench;singleton:=true
+Bundle-ActivationPolicy: lazy
+
+Fragment-Host: org.eclipse.rap.ui.workbench
diff --git a/org.argeo.ext.rap.ui.workbench/build.properties b/org.argeo.ext.rap.ui.workbench/build.properties
new file mode 100644 (file)
index 0000000..485b266
--- /dev/null
@@ -0,0 +1,2 @@
+source.. = src/,\
+           ext/test/
diff --git a/org.argeo.ext.rap.ui.workbench/pom.xml b/org.argeo.ext.rap.ui.workbench/pom.xml
new file mode 100644 (file)
index 0000000..b8c3755
--- /dev/null
@@ -0,0 +1,11 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>argeo-commons</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.ext.rap.ui.workbench</artifactId>
+       <name>Extension RAP Workbench</name>
+</project>
diff --git a/org.argeo.jcr/.classpath b/org.argeo.jcr/.classpath
new file mode 100644 (file)
index 0000000..a8a298a
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="src" path="ext/test"/>
+       <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-1.8"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.jcr/.gitignore b/org.argeo.jcr/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.jcr/.project b/org.argeo.jcr/.project
new file mode 100644 (file)
index 0000000..e432547
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.jcr</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.jcr/META-INF/.gitignore b/org.argeo.jcr/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.jcr/bnd.bnd b/org.argeo.jcr/bnd.bnd
new file mode 100644 (file)
index 0000000..9183b9a
--- /dev/null
@@ -0,0 +1,14 @@
+#Provide-Capability: cms.datamodel;name=docbook;cnd="/org/argeo/jcr/docbook/docbook.cnd"
+
+Import-Package: junit.framework;resolution:=optional,\
+org.xml.sax;version="0.0.0",\
+org.springframework.core;resolution:=optional,\
+org.springframework.core.io;resolution:=optional,\
+org.springframework.*;resolution:=optional,\
+org.apache.jackrabbit.*;resolution:=optional,\
+org.apache.jackrabbit.webdav.jcr;resolution:=optional,\
+org.apache.jackrabbit.webdav.server;resolution:=optional,\
+org.h2;resolution:=optional,\
+org.postgresql;resolution:=optional,\
+*
+Export-Package: org.argeo.jcr.*, org.argeo.jackrabbit.*
\ No newline at end of file
diff --git a/org.argeo.jcr/build.properties b/org.argeo.jcr/build.properties
new file mode 100644 (file)
index 0000000..8a9736e
--- /dev/null
@@ -0,0 +1,29 @@
+source.. = src/,\
+           ext/test/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
+additional.bundles = org.junit,\
+                     org.apache.jackrabbit.core,\
+                     javax.jcr,\
+                     org.apache.jackrabbit.api,\
+                     org.apache.jackrabbit.data,\
+                     org.apache.jackrabbit.jcr.commons,\
+                     org.apache.jackrabbit.spi,\
+                     org.apache.jackrabbit.spi.commons,\
+                     org.slf4j.api,\
+                     org.slf4j.commons.logging,\
+                     org.slf4j.log4j12,\
+                     org.apache.log4j,\
+                     org.apache.commons.collections,\
+                     EDU.oswego.cs.dl.util.concurrent,\
+                     org.apache.lucene,\
+                     org.apache.tika.core,\
+                     org.apache.commons.dbcp,\
+                     org.apache.commons.pool,\
+                     com.google.guava,\
+                     org.apache.jackrabbit.jcr2spi,\
+                     org.apache.jackrabbit.spi2dav,\
+                     org.apache.httpcomponents.httpclient,\
+                     org.apache.httpcomponents.httpcore,\
+                     org.apache.tika.parsers
diff --git a/org.argeo.jcr/ext/test/log4j.properties b/org.argeo.jcr/ext/test/log4j.properties
new file mode 100644 (file)
index 0000000..3d75289
--- /dev/null
@@ -0,0 +1,14 @@
+log4j.rootLogger=WARN, console
+
+## Levels
+log4j.logger.org.argeo=DEBUG
+log4j.logger.org.apache.jackrabbit=OFF
+
+## Appenders
+# console is set to be a ConsoleAppender.
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+
+# console uses PatternLayout.
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+#log4j.appender.console.layout.ConversionPattern= %-5p %d{ISO8601} %m - %c%n
+log4j.appender.console.layout.ConversionPattern=%m%n
diff --git a/org.argeo.jcr/ext/test/org/argeo/jcr/docbook/DocBookModelTest.java b/org.argeo.jcr/ext/test/org/argeo/jcr/docbook/DocBookModelTest.java
new file mode 100644 (file)
index 0000000..5af20ba
--- /dev/null
@@ -0,0 +1,115 @@
+package org.argeo.jcr.docbook;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.argeo.jackrabbit.unit.AbstractJackrabbitTestCase;
+import org.argeo.jcr.JcrUtils;
+
+public class DocBookModelTest extends AbstractJackrabbitTestCase {
+       private final static Log log = LogFactory.getLog(DocBookModelTest.class);
+
+       public void testLoadWikipediaSample() throws Exception {
+               importXml("WikipediaSample.dbk.xml");
+       }
+
+       public void XXXtestLoadHowTo() throws Exception {
+               importXml("howto.xml", false);
+       }
+
+       protected void importXml(String res) throws Exception {
+               importXml(res, true);
+       }
+
+       protected void importXml(String res, Boolean mini) throws Exception {
+               byte[] bytes;
+               try (InputStream in = getClass().getResourceAsStream(res)) {
+                       bytes = IOUtils.toByteArray(in);
+               }
+
+               {// cnd
+                       long begin = System.currentTimeMillis();
+                       if (mini) {
+                               InputStreamReader reader = new InputStreamReader(getClass()
+                                               .getResourceAsStream(
+                                                               "/org/argeo/jcr/docbook/docbook.cnd"));
+                               CndImporter.registerNodeTypes(reader, session());
+                               reader.close();
+                       } else {
+                               InputStreamReader reader = new InputStreamReader(getClass()
+                                               .getResourceAsStream(
+                                                               "/org/argeo/jcr/docbook/docbook-full.cnd"));
+                               CndImporter.registerNodeTypes(reader, session());
+                               reader.close();
+                       }
+                       long duration = System.currentTimeMillis() - begin;
+                       if (log.isDebugEnabled())
+                               log.debug(" CND loaded in " + duration + " ms");
+               }
+
+               String testPath = "/" + res;
+               // if (mini)
+               JcrUtils.mkdirs(session(), testPath, "dbk:set");
+               // else
+               // JcrUtils.mkdirs(session(), testPath, "dbk:book");
+
+               DocBookModel model = new DocBookModel(session());
+               try (InputStream in = new ByteArrayInputStream(bytes)) {
+                       long begin = System.currentTimeMillis();
+                       model.importXml(testPath, in);
+                       long duration = System.currentTimeMillis() - begin;
+                       if (log.isDebugEnabled())
+                               log.debug("Imported " + res + " " + (bytes.length / 1024l)
+                                               + " kB in " + duration + " ms ("
+                                               + (bytes.length / duration) + " B/ms)");
+               }
+
+               saveSession();
+               // JcrUtils.debug(session().getRootNode());
+
+               try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+                       try {
+                               model.exportXml(testPath + "/dbk:book", out);
+                       } catch (Exception e) {
+                               model.exportXml(testPath + "/dbk:article", out);
+                       }
+                       bytes = out.toByteArray();
+
+                       session().logout();
+                       model.setSession(session());
+
+                       // log.debug(new String(bytes));
+                       try (InputStream in = new ByteArrayInputStream(bytes)) {
+                               long begin = System.currentTimeMillis();
+                               model.importXml(testPath, in);
+                               long duration = System.currentTimeMillis() - begin;
+                               if (log.isDebugEnabled())
+                                       log.debug("Re-imported " + res + " "
+                                                       + (bytes.length / 1024l) + " kB in " + duration
+                                                       + " ms (" + (bytes.length / duration) + " B/ms)");
+                       }
+               }
+               saveSession();
+       }
+
+       protected void saveSession() throws RepositoryException {
+               long begin = System.currentTimeMillis();
+               session().save();
+               long duration = System.currentTimeMillis() - begin;
+               if (log.isDebugEnabled())
+                       log.debug(" Session save took " + duration + " ms");
+       }
+
+       // public static Test suite() {
+       // return defaultTestSuite(DocBookModelTest.class);
+       // }
+
+}
diff --git a/org.argeo.jcr/ext/test/org/argeo/jcr/docbook/WikipediaSample.dbk.xml b/org.argeo.jcr/ext/test/org/argeo/jcr/docbook/WikipediaSample.dbk.xml
new file mode 100644 (file)
index 0000000..29f5b70
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<book xmlns="http://docbook.org/ns/docbook">
+       <title>Very simple book</title>
+       <chapter>
+               <title>Chapter 1</title>
+               <para>Hello world!</para>
+               <para>I hope that your day is proceeding <emphasis>splendidly</emphasis>!</para>
+       </chapter>
+       <chapter>
+               <title>Chapter 2</title>
+               <para>Hello again, world!</para>
+       </chapter>
+</book>
\ No newline at end of file
diff --git a/org.argeo.jcr/ext/test/org/argeo/jcr/docbook/howto.xml b/org.argeo.jcr/ext/test/org/argeo/jcr/docbook/howto.xml
new file mode 100644 (file)
index 0000000..b8b022a
--- /dev/null
@@ -0,0 +1,2295 @@
+<?xml version="1.0" encoding="utf-8"?>  <!-- -*- nxml -*- -->
+<!DOCTYPE book [
+<!ENTITY version "5.0">
+<!--
+<!ENTITY yes "<phrase dbk:role='unicode yes'>✔</phrase>">
+<!ENTITY no "<phrase dbk:role='unicode no'>✘</phrase>">
+-->
+<!ENTITY yes "<phrase dbk:role='unicode yes'>YES</phrase>">
+<!ENTITY no "<phrase dbk:role='unicode no'>NO</phrase>">
+]>
+<book xmlns="http://docbook.org/ns/docbook"  xmlns:dbk="http://docbook.org/ns/docbook"
+        xmlns:xl="http://www.w3.org/1999/xlink" xml:lang="en">
+<article>
+<info>
+<title>DocBook V5.0</title>
+<subtitle>The Transition Guide</subtitle>
+
+<authorgroup>
+<author><personname>Jirka Kosek</personname>
+        <email>jirka@kosek.cz</email></author>
+<author><personname>Norman Walsh</personname>
+        <email>ndw@nwalsh.com</email>
+        <contrib>§convert4to5, proofreading</contrib></author>
+<author><personname>Dick Hamilton</personname>
+        <email>rlhamilton@frii.com</email>
+        <contrib>§changes-removed, customization, proofreading</contrib></author>
+<othercredit
+  dbk:class="other"
+  dbk:otherclass="contributor"
+  ><personname>Michael(tm) Smith</personname>
+  <email>smith@sideshowbarker.net</email>
+  <contrib>§dbxsl-ns</contrib>
+</othercredit>
+</authorgroup>
+
+<pubdate>2009-06-16</pubdate>
+<pubdate>2008-02-06</pubdate>
+<pubdate>2007-10-28</pubdate>
+<pubdate>2006-10-22</pubdate>
+<pubdate>2006-05-16</pubdate>
+<pubdate>2006-03-01</pubdate>
+<pubdate>2005-12-28</pubdate>
+<pubdate>2005-10-27</pubdate>
+
+</info>
+
+<para>This document is targeted at DocBook users who are considering
+switching from DocBook V4.x to DocBook V5.0. It describes
+differences between DocBook V4.x and V5.0 and provides some suggestions about
+how to edit and process DocBook V5.0 documents. There is
+also a section devoted to conversion of legacy documents from DocBook
+4.x to DocBook V5.0.</para>
+
+<para>At the time this was written the current version of DocBook V5.0
+was &version;. However, almost all of the information in this document is
+general and applies to any newer version of DocBook V5.0.
+</para>
+
+<section xml:id="introduction">
+<title>Introduction</title>
+
+<para>The differences between DocBook V4.x and V5.0 are quite radical in
+some aspects, but the basic idea behind DocBook is still the same, and
+almost all element names are unchanged. Because of this it is very
+easy to become familiar with DocBook V5.0 if you know any previous version of
+DocBook. You can find a complete list of changes in
+<citation>DB5SPEC</citation>, here we will discuss only the most
+fundamental changes.</para>
+
+<section xml:id="introduction-ns">
+<title>Finally in a namespace</title>
+
+<para>All DocBook V5.0 elements are in the namespace
+<uri>http://docbook.org/ns/docbook</uri>. <acronym>XML<alt>Extensible
+Markup Language</alt></acronym> namespaces are used to distinguish
+between different element sets. In the last few years, almost all new
+XML grammars have used their own namespace. It is easy to
+create compound documents that contain elements from different XML
+vocabularies. DocBook V5.0 is following this design rule. Using
+namespaces in your documents is very easy. Consider this
+simple article marked up in DocBook V4.5:</para>
+
+<programlisting><![CDATA[<article>
+  <title>Sample article</title>
+  <para>This is a really short article.</para>
+</article>]]></programlisting>
+
+<para>The corresponding DocBook V5.0 article will look very similar:</para>
+
+<programlisting><![CDATA[<article xmlns="http://docbook.org/ns/docbook" …>
+  <title>Sample article</title>
+  <para>This is a really short article.</para>
+</article>]]></programlisting>
+
+<para>The only change is the addition of a default namespace declaration
+(<code>xmlns="http://docbook.org/ns/docbook"</code>) on the root
+element. This declaration applies the namespace to the root element and
+all nested elements. Each
+element is now uniquely identified by its local name and namespace.</para>
+
+<note>
+<para>The namespace name <uri>http://docbook.org/ns/docbook</uri> serves
+only as an identifier. This resource is not fetched during processing
+of DocBook documents, and you are not required to have an Internet
+connection during processing. If you access the namespace URI with a browser,
+you will find a short explanatory document about the namespace. In the
+future this document will probably conform to (some version of) RDDL
+and provide pointers to related resources.</para>
+</note>
+
+</section>
+
+<section xml:id="introduction-rng">
+<title>Relaxing with DocBook</title>
+
+<para>For more than a decade, the DocBook schema was defined using a
+DTD. However, DTDs have serious limitations, and DocBook V5.0 is thus
+defined using a very powerful schema language called RELAX NG. Thanks
+to RELAX NG, it is now much easier to create customized versions of
+DocBook, and some content models are now cleaner and more
+precise.</para>
+
+<para>Using RELAX NG has an impact on the document prolog. The following
+example shows the typical prolog of a DocBook V4.x document. The version of
+the DocBook DTD (in this case 4.5) is indicated in the document type
+declaration (!DOCTYPE) which points to a particular version of the
+DTD.</para>
+
+<example xml:id="ex.docbook45">
+<title>DocBook V4.5 document</title>
+<programlisting><![CDATA[<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE article PUBLIC '-//OASIS//DTD DocBook XML V4.5//EN'
+                         'http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd'>
+<article lang="en">
+  <title>Sample article</title>
+  <para>This is a very short article.</para>
+</article>]]></programlisting>
+</example>
+
+<para>In contrast, DocBook V5.0 does not depend on DTDs anymore. This
+mean that there is no document type declaration and the version of DocBook
+used is indicated with the <tag dbk:class="attribute">version</tag>
+attribute instead.</para>
+
+<example xml:id="ex.docbook5">
+<title>DocBook V5.0 document</title>
+<programlisting><![CDATA[<?xml version="1.0" encoding="utf-8"?>
+<article xmlns="http://docbook.org/ns/docbook" version="5.0" xml:lang="en">
+  <title>Sample article</title>
+  <para>This is a very short article.</para>
+</article>]]></programlisting>
+</example>
+
+<para>As you can see, DocBook V5.0 is built on top of existing XML
+standards as much as possible, for example the <tag
+dbk:class="attribute">lang</tag> attribute is superseded by the standard
+<tag xl:href="http://www.w3.org/TR/REC-xml/#sec-lang-tag"
+dbk:class="attribute">xml:lang</tag> attribute.</para>
+
+<para>Another fundamental change is that there is no direct indication
+of the schema used. Later in this document, you will learn how you can
+specify a schema to be used for document validation.</para>
+
+<note>
+<para>Although we recommend the RELAX NG schema for DocBook
+V5.0, there are also DTD and W3C XML Schema versions available (see <xref
+dbk:linkend="schemas"/>) for tools that do not yet support RELAX NG.</para>
+</note>
+
+</section>
+
+<section xml:id="introduction-why-to-switch">
+<title>Why switch to DocBook V5.0?</title>
+
+<para>The simple answer is <quote>because DocBook V5.0 is the
+future</quote>. Apart from this marketing blurb, there are also more
+technical reasons:</para>
+
+<itemizedlist>
+<listitem>
+<para><emphasis>DocBook V4.x is feature frozen.</emphasis>DocBook V4.5
+is the last version of DocBook in the V4.x series. Any new DocBook
+development, like the addition of new elements, will be done in
+DocBook V5.0. It is only matter of time before useful, new elements
+will be added into DocBook V5.0, but they are not likely to be back
+ported into DocBook V4.x. DocBook V4.x will be in maintenance mode and
+errata will be published if necessary. </para>
+</listitem>
+<listitem>
+<para><emphasis>DocBook V5.0 offers new functionality.</emphasis>
+DocBook V5.0 provides significant improvements over DocBook V4.x. For
+example there is general markup for annotations, a new and flexible
+system for linking, and unified markup for information sections using
+the <tag>info</tag> element.</para>
+</listitem>
+<listitem>
+<para><emphasis>DocBook V5.0 is more extensible.</emphasis> Having
+DocBook V5.0 in a separate namespace allows you to easily mix DocBook
+markup with other XML-based languages like SVG, MathML, XHTML or even
+FooBarML.</para>
+</listitem>
+<listitem>
+<para><emphasis>DocBook V5.0 is easier to customize.</emphasis> RELAX
+NG offers many powerful constructs that make customization much easier
+than it would be using a DTD (see <xref dbk:linkend="customizations"/>).</para>
+</listitem>
+</itemizedlist>
+
+</section>
+
+<section xml:id="introduction-schemas">
+<title>Schema jungle</title>
+
+<para>Schemas for DocBook V5.0 are available in several formats at
+<link xl:href="http://www.oasis-open.org/docbook/xml/&version;/"/> (or the
+mirror at <link xl:href="http://docbook.org/xml/&version;/"/>).
+Only the RELAX NG schema is normative
+and it is preferred over the other schema languages.  However, for your
+convenience there are also DTD and W3C XML Schema versions provided for DocBook
+V5.0. But please note that neither the DTD nor the W3C XML schema are able to
+capture all the constraints of DocBook V5.0. This mean that a
+document that validates against the DTD or XML schema is not necessarily
+valid against the RELAX NG schema and thus may not be a valid
+DocBook V5.0 document. See <xref dbk:linkend="t.schema-comparison"/> for
+summary of constraints that are checked by different schemas.</para>
+
+<para>DTD and W3C XML Schema versions of the DocBook V5.0 grammar are provided
+as a convenience for users who want to use DocBook V5.0 with legacy tools
+that don't support RELAX NG. Authors are encouraged to switch to RELAX
+NG based tools as soon as possible, or at least to validate documents
+against the RELAX NG schema before further processing.</para>
+
+<para>Some document constraints can't be expressed in schema languages
+like RELAX NG or W3C XML Schema. To check for these additional
+constraints DocBook V5.0 uses Schematron.  We recommend that you
+validate your document against both the RELAX NG and
+Schematron schemas.</para>
+
+<table xml:id="t.schema-comparison">
+  <title>Schema Comparison</title>
+  <tgroup dbk:cols="6">
+    <colspec dbk:colwidth="4*"/>
+    <colspec dbk:colwidth="1*" dbk:align="center"/>
+    <colspec dbk:colwidth="1*" dbk:align="center"/>
+    <colspec dbk:colwidth="1*" dbk:align="center"/>
+    <colspec dbk:colwidth="1*" dbk:align="center"/>
+    <colspec dbk:colwidth="1*" dbk:align="center"/>
+    <thead>
+      <row>
+       <entry>Description</entry>
+       <entry>DTD</entry>
+       <entry>W3C XML Schema</entry>
+       <entry>W3C XML Schema + Schematron</entry>
+       <entry>RELAX NG</entry>
+       <entry>RELAX NG + Schematron/NVDL</entry>
+      </row>
+    </thead>
+    <tbody>
+      <row>
+       <entry>Basic document structure</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+      </row>
+      <row>
+       <entry>ID/IDREF datatypes</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+      </row>
+      <row>
+       <entry>Datatypes<footnote>
+         <para>In a very few places RELAX NG specifies datatype
+         like number (mainly for length specifications) or
+         enumeration between <literal>0</literal> and
+         <literal>1</literal>.</para>
+         <para>In general those datatypes can be also supported in
+         W3C XML Schema, but currently this schema is generated
+         from DTD which lacks datatype information.</para>
+       </footnote>
+       </entry>
+       <entry>&no;</entry>
+       <entry>&no;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+      </row>
+      <row>
+       <entry>Co-occurrences<footnote>
+       <para>RELAX NG grammar enforces exclusivity of several
+       elements. For example if you have <tag>title</tag> inside
+       <tag>info</tag> then it is not allowed to have another
+       <tag>title</tag> outside <tag>info</tag>. Similarly,
+       models of HTML and CALS tables are separated and validated
+       properly, where in DTD and WXS only union of both models is
+       available.</para>
+       <para>On other places co-occurrences enforces particular
+       content model based on presence of specific attribute or
+       attribute value.</para>
+       <para>Please also note that in theory co-occurences can be
+       validated using Schematron, but the current DocBook schema
+       uses RELAX NG for these definitions. Schematron can be used
+       only for validation, whereas grammar based schemas like
+       RELAX NG are useful also for other purposes like guided editing.</para>
+       </footnote></entry>
+       <entry>&no;</entry>
+       <entry>&no;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+      </row>
+      <row>
+       <entry>Hooks for MathML and SVG content</entry>
+       <entry>&no;</entry>
+       <entry>&no;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+       <entry>&yes;</entry>
+      </row>
+      <row>
+       <entry>Link type integrity<footnote>
+       <para>Check whether ID/IDREF links are pointing to element
+       of corresponding type. For example that
+       <tag>footnoteref</tag> points to
+       <tag>footnote</tag>.</para></footnote></entry>
+       <entry>&no;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+      </row>
+      <row>
+       <entry>Presence of <tag dbk:class="attribute">version</tag>
+       attribute on the root element</entry>
+       <entry>&no;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+      </row>
+      <row>
+       <entry>Miscellaneous checks<footnote>
+       <para>For example consistency of segmented lists, only one
+       term inside term definition etc.</para></footnote></entry>
+       <entry>&no;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+      </row>
+      <row>
+       <entry>Element exclusions<footnote>
+       <para>Prevents improper nesting of elements, like admonition
+       inside admonition.</para></footnote></entry>
+       <entry>&no;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+       <entry>&no;</entry>
+       <entry>&yes;</entry>
+      </row>        
+    </tbody>
+  </tgroup>
+</table>
+
+<section xml:id="schemas">
+<title>Where to get the schemas</title>
+
+<para>The latest versions of schemas can be obtained from <link
+xl:href="http://docbook.org/schemas/5x.html"/>. At the time this was
+written the latest version was &version;. Individual schemas are
+available at the following locations:</para>
+
+<variablelist>
+<varlistentry>
+<term>RELAX NG schema</term>
+<listitem><para><link xl:href="http://docbook.org/xml/&version;/rng/docbook.rng"/></para></listitem>
+</varlistentry>
+<varlistentry>
+<term>RELAX NG schema in compact syntax</term>
+<listitem><para><link xl:href="http://docbook.org/xml/&version;/rng/docbook.rnc"/></para></listitem>
+</varlistentry>
+<varlistentry>
+<term>DTD</term>
+<listitem><para><link xl:href="http://docbook.org/xml/&version;/dtd/docbook.dtd"/></para></listitem>
+</varlistentry>
+<varlistentry>
+<term>W3C XML Schema</term>
+<listitem><para><link xl:href="http://docbook.org/xml/&version;/xsd/docbook.xsd"/></para></listitem>
+</varlistentry>
+<varlistentry>
+<term>Schematron schema with additional checks</term>
+<listitem><para><link xl:href="http://docbook.org/xml/&version;/sch/docbook.sch"/></para></listitem>
+</varlistentry>
+</variablelist>
+
+<para>These schemas are also available from the mirror at
+<link xl:href="http://www.oasis-open.org/docbook/xml/&version;/"/>.</para>
+
+</section>
+
+<section xml:id="docs">
+<title>DocBook documentation</title>
+
+<para>Detailed documentation about each DocBook V5.0 element is
+presented in <link
+xl:href="http://docbook.org/tdg5/en/html/pt02.html">the reference part
+of <citetitle>DocBook: The Definitive Guide</citetitle></link>.</para>
+
+<note>
+<para>Other parts of <citetitle>DocBook: The Definitive
+Guide</citetitle> have not yet been updated to reflect the changes
+made in DocBook V5.0. Please do not be confused by this.</para>
+</note>
+
+</section>
+
+</section>
+
+</section>
+
+<section xml:id="tools">
+<title>Tool chain</title>
+
+<para>This section briefly describes tools and procedures to edit and
+process content stored in DocBook V5.0.</para>
+
+<section xml:id="editors">
+<title>Editing DocBook V5.0</title>
+
+<para>Because DocBook is an XML-based format and XML is a text-based
+format, you can use any text editor to create and edit DocBook V5.0
+documents. However, using <quote>dumb</quote> editors like Notepad is
+not very productive. You will do better if you use an editor that
+supports XML. Although there are DTD and W3C XML Schemas available for
+DocBook V5.0, which means you can use any editor that works with DTDs
+or W3C XML Schemas, we recommend that you use the RELAX NG grammar
+with DocBook V5.0. The rest of this section contains an overview of
+XML editors (listed in alphabetical order) that are known to work with
+RELAX NG schemas and that offer guided editing based on the RELAX NG
+schema.</para>
+
+<section xml:id="editors-nxml">
+<title>Emacs and nXML</title>
+
+<para><link xl:href="http://www.thaiopensource.com/nxml-mode/">nXML
+mode</link> is an add-on for the <application
+xl:href="http://www.gnu.org/software/emacs/emacs.html">GNU
+Emacs</application> text editor. By installing nXML you can turn Emacs
+into a very powerful XML editor that offers guided editing and
+validation of XML documents.</para>
+
+<figure xml:id="f.emacs">
+<title>Emacs with nXML mode provides guided editing and validation</title>
+<mediaobject>
+<imageobject dbk:role="html">
+<imagedata dbk:fileref="images/emacs.png"/>
+</imageobject>
+<imageobject dbk:role="fo">
+<imagedata dbk:fileref="images/emacs.png" dbk:width="100%"/>
+</imageobject>
+</mediaobject>
+</figure>
+
+<para>nXML uses a special configuration file named
+<filename>schemas.xml</filename> to associate schemas with XML
+documents. Often you will find this file in the directory
+<filename>site-lisp/nxml/schema</filename> inside the Emacs installation
+directory. Adding the following line into the configuration file,
+will associate DocBook V5.0 elements with the appropriate
+schema:</para>
+
+<programlisting>&lt;namespace ns="http://docbook.org/ns/docbook" uri="<replaceable>/path/to/</replaceable>docbook.rnc"/></programlisting>
+
+<note>
+<para>Please note that nXML ships with a file named
+<filename>docbook.rnc</filename>. This file contains the RELAX NG grammar
+for DocBook V4.x. Be sure that you associate the DocBook V5.0 namespace
+with the corresponding DocBook V5.0 grammar.</para>
+</note>
+
+<para>If you can't edit the global <filename>schemas.xml</filename> file,
+you can create this file in the same directory as your document. nXML will
+find associations placed there also. In this case you must create a
+complete configuration file like:</para>
+
+<programlisting>&lt;locatingRules xmlns="http://thaiopensource.com/ns/locating-rules/1.0">
+  &lt;namespace ns="http://docbook.org/ns/docbook" uri="<replaceable>/path/to/</replaceable>docbook.rnc"/>
+&lt;/locatingRules></programlisting>
+
+</section>
+
+<section xml:id="editors-oxygen">
+<title>oXygen</title>
+
+<para><application
+xl:href="http://www.oxygenxml.com/">oXygen</application> is a feature
+rich XML editor. It has built-in support for many schema languages
+including RELAX NG and it is preconfigured with many document types
+including DocBook. oXygen will assist you with writing DocBook V5.0
+content, and you will be able to validate your documents against both
+RELAX NG and Schematron schemas.</para>
+
+<figure xml:id="f.oxygen.open5">
+<title>DocBook V5.0 document opened in oXygen</title>
+<mediaobject>
+<imageobject>
+<imagedata dbk:fileref="images/oxygen4.png" dbk:width="100%"/>
+</imageobject>
+</mediaobject>
+</figure>
+
+<figure xml:id="f.oxygen.author.mode">
+<title>DocBook V5.0 document opened in oXygen in Author mode</title>
+<mediaobject>
+<imageobject>
+<imagedata dbk:fileref="images/oxygen5.png" dbk:width="100%"/>
+</imageobject>
+</mediaobject>
+</figure>
+
+</section>
+
+<section xml:id="editors-xxe">
+<title>XML Mind XML editor</title>
+
+<para><application xl:href="http://www.xmlmind.com/xmleditor/">XML
+Mind XML editor</application> (XXE) is a visual validating XML editor that
+provides a wordprocessor-like interface to users. It is available in
+two versions, Standard and Professional. The Standard version is free and
+provides everything you need to edit DocBook V5.0 documents.</para>
+
+<figure xml:id="f.xmlmind">
+<title>XML Mind XML Editor – feels almost like MS Word but real DocBook V5.0 markup is created</title>
+<mediaobject>
+<imageobject>
+<imagedata dbk:fileref="images/xxe.png" dbk:width="100%"/>
+</imageobject>
+</mediaobject>
+</figure>
+
+<para>In order to use DocBook V5.0 in XXE you have to install
+an add-on. Go to
+<menuchoice><guimenu>Options</guimenu><guimenuitem>Install
+Add-ons…</guimenuitem></menuchoice>. Then choose <guilabel>DocBook
+5 configuration</guilabel> and press the <guibutton>OK</guibutton>
+button. After restart, XXE is ready to work with DocBook V5.0
+documents.</para>
+
+</section>
+
+</section>
+
+<section xml:id="validators">
+<title>Validating DocBook V5.0</title>
+
+<para>If you are not using a RELAX NG-based validating editor when you
+create documents, we strongly recommend that you validate your
+documents against RELAX NG and Schematron schemas before processing
+them. Only after successful validation can you be sure that your
+document is really DocBook V5.0 and that processing tools will be able
+to process it correctly.</para>
+
+<para>For validation you can use tools that support simultaneous RELAX NG and
+Schematron validation, or you can use NVDL to orchestrate validation using
+the two schemas.</para>
+
+<section xml:id="validators-rng-sch">
+<title>Using RELAX NG and Schematron</title>
+
+<para>You can find a list of RELAX NG validators at <link
+xl:href="http://relaxng.org/#validators"/>. It is best to use
+validators with support for embedded Schematron rules inside RELAX NG
+schemas. Schematron is a rule-based validation language which is used
+to impose additional constraints on DocBook documents. Schematron rules
+assert conditions which are impossible or difficult to express 
+in a pure RELAX NG schema.</para>
+
+<para><application xl:href="https://msv.dev.java.net/">Sun 
+Multi-Schema XML Validator (MSV)</application> is able to validate an XML
+document against a RELAX NG schema and Schematron rules at the same time.
+To install and use MSV follow these steps:</para>
+
+<procedure>
+<step>
+<para>Download <filename>relames.zip</filename> from <link xl:href="https://msv.dev.java.net/servlets/ProjectDocumentList?folderID=101"/>.</para>
+</step>
+<step>
+<para>Unpack the downloaded file into an arbitrary directory.</para>
+</step>
+<step>
+<para>Validate your document using the following command:</para>
+<screen><command>java</command> -Xss512K -jar <replaceable>/path/to/</replaceable>relames.jar <replaceable>/path/to/</replaceable>docbook.rng document.xml</screen>
+<note>
+<para>The switch <option>-Xss512K</option> increases the stack size
+of the Java virtual machine. This is necessary because the DocBook schema is
+quite large. If you get stack overflow errors from MSV, increase
+this value. You may get spurious error messages if the value
+is too small, so if you get a stack overflow error, ignore any other error
+messages and try a larger value for the stack size.
+If you are not using Sun's Java implementation, please consult the
+documentation for your virtual machine to learn how to increase the stack
+size.</para>
+</note>
+</step>
+</procedure>
+
+<para>There is also an <link
+xl:href="http://relaxed.vse.cz/docbookvalidator/">on-line DocBook V5.0
+validator</link> that validates DocBook V5.0 documents against the normative
+RELAX NG schema with embedded Schematron rules.</para>
+
+</section>
+
+<section>
+<title>Using NVDL</title>
+
+<para>NVDL is a meta-schema language which can validate a document 
+against several schemas. DocBook V5.0 comes with a NVDL
+schema which specifies that DocBook documents should be validated
+against both RELAX NG and Schematron schemas.</para>
+
+<para>You can find a list of NVDL validators at <link
+xl:href="http://nvdl.org/"/>. The following procedures show how to
+install and use the <application
+xl:href="http://www.oxygenxml.com/onvdl.html">oNVDL</application> and
+<application xl:href="http://jnvdl.sourceforge.net">JNVDL</application>
+validators.</para>
+
+<procedure>
+<title>oNVDL installation and usage</title>
+<step>
+<para>Download <filename
+xl:href="http://www.oxygenxml.com/InstData/onvdl/onvdl-20070517.zip">onvdl-20070517.zip</filename>.</para>
+</step>
+<step>
+<para>Unpack the downloaded file into an arbitrary directory.</para>
+</step>
+<step>
+<para>Validate your document using the following command:</para>
+<screen><command>java</command> -jar <replaceable>/path/to/oNVDL/</replaceable>bin/onvdl.jar <replaceable>/path/to/</replaceable>docbook.nvdl document.xml</screen>
+</step>
+</procedure>
+
+<procedure>
+<title>JNVDL installation and usage</title>
+<step>
+<para>Download the latest release of JNVDL from <link
+xl:href="http://sourceforge.net/project/showfiles.php?group_id=164464"/>.</para>
+</step>
+<step>
+<para>Unpack the downloaded file into an arbitrary directory.</para>
+</step>
+<step>
+<para>Modify file <filename>jnvdl.bat</filename> (or <filename>jnvdl.sh</filename> on Unix based systems) to include <option>-Xss512K</option> switch directly after <command>java</command> command.</para>
+</step>
+<step>
+<para>On Windows systems, validate your document using the following command:</para>
+<screen><replaceable>/path/to/jnvdl/</replaceable><command>jnvdl</command> -nt -s <replaceable>/path/to/</replaceable>docbook.nvdl document.xml</screen>
+<para>On Unix systems, validate your document using the following command:</para>
+<screen><replaceable>/path/to/jnvdl/</replaceable><command>jnvdl.sh</command> -nt -s <replaceable>/path/to/</replaceable>docbook.nvdl document.xml</screen>
+</step>
+</procedure>
+
+</section>
+
+</section>
+
+<section xml:id="processing">
+<title>Processing DocBook V5.0</title>
+
+<para>Part of DocBook's great success can be attributed to the
+availability of free
+tools that can be used to transform DocBook content into various
+target formats including HTML and PDF. The DocBook XSL Stylesheets are
+very popular tools.</para>
+
+<section xml:id="dbxsl">
+<title>DocBook XSL Stylesheets</title>
+
+<para>The DocBook stylesheets are designed to process content written in
+different versions of DocBook (for example 3.1 and 4.2). Recent
+versions of the stylesheets are also able to process DocBook V5.0
+with some limitations.</para>
+
+<para>You can process DocBook V5.0 documents with the DocBook XSL
+stylesheets in exactly the same way you process DocBook V4.x documents.
+You do not need special software; you can stick to your preferred
+XSLT processor, be it Saxon, xsltproc, Xalan or whatever else (but see
+the note about the lost base URI below).</para>
+
+<para>During document processing, the stylesheets strip
+namespaces from DocBook V5.0 to get a document which will be
+very similar to DocBook V4.x. This is necessary because from the XSLT
+point of view, elements from different namespaces are distinct and cannot 
+be easily processed by the same set of templates. This process is
+completely transparent to the user. If you are processing DocBook V5.0
+documents, the only difference is that you will see the following
+additional message:</para>
+
+<screen>Note: namesp. cut : stripped namespace before processing
+Note: namesp. cut : processing stripped document</screen>
+
+<para>Although you can successfully use the existing stylesheets to
+process DocBook V5.0, there are some limitations and unsupported
+features. The unsupported features include:</para>
+
+<itemizedlist>
+<listitem><para>general annotations;</para></listitem>
+<listitem><para>general XLink links on all elements.</para></listitem>
+</itemizedlist>
+
+<note>
+<para>During namespace stripping, the base URI of the document is
+lost. This means that in rare situations, relatively referenced
+resources like images or programlistings can be processed incorrectly.
+The stylesheets attempt to compensate for this problem, but that is not always 
+possible. When an XSLT processor other than Saxon or Xalan is used, a warning 
+message is generated:
+
+<screen>WARNING: cannot add @xml:base to node set root element. Relative paths may not work.</screen>
+</para>
+
+</note>
+</section>
+
+<section xml:id="dbxsl-ns">
+<title>DocBook XSL-NS Stylesheets</title>
+<para>As you can see from reading the previous section, namespace
+  stripping has limitations that will cause trouble in some
+  situations. To overcome those limitations, Bob Stayton created a
+  build system for taking the non-namespace-aware DocBook XSL
+  stylesheets and generating namespace-aware versions from them.
+  The DocBook <link
+    xl:href="http://docbook.sourceforge.net/release/xsl-ns/current/"
+  >XSL-NS stylesheets</link> are the result.</para>
+
+<para>The DocBook XSL-NS stylesheets are released side-by-side
+  with the DocBook XSL stylesheets, as a separate <link
+  xl:href="https://sourceforge.net/project/showfiles.php?group_id=21935&amp;package_id=219178"
+  ><package>docbook-xsl-ns</package></link> package. They are the
+recommended XSLT 1.0 stylesheets to use for transforming
+namespaced (DocBook V5.0) documents.</para>
+</section>
+
+<section xml:id="dbxsl2">
+<title>XSLT 2.0 based re-implementation</title>
+
+<para>XSLT 1.0 is missing some important features. To work around
+these missing features, the current DocBook XSL stylesheets use some
+implementation-specific extensions.
+XSLT 2.0 adds many new and previously missing features into the language.
+A new set of DocBook stylesheets is being implemented based on XSLT 2.0
+to take advantage of these features and to fully support DocBook V5.0.
+</para>
+
+<para>The XSLT 2.0 based stylesheets have many new features, including:</para>
+
+<itemizedlist>
+<listitem><para>seamless integration of profiling (conditional
+documents) with external bibliographies and
+glossaries;</para></listitem>
+<listitem><para>no need for (most) external extensions;</para></listitem>
+<listitem><para>internationalized indexes;</para></listitem>
+<listitem><para>easy to customize titlepage templates.</para></listitem>
+</itemizedlist>
+
+<para>The XSLT 2.0 based stylesheets are still under development.  At
+this writing, they only support HTML and chunked HTML output.  As time
+permits, the stylesheet developers will be adding other formats.  Since
+the stylesheets are developed in the limited free time the developers
+have, there's no specific schedule.</para>
+
+<para>There are not very many XSLT 2.0 implementations available.
+But, if you want to try the new stylesheets, grab a snapshot of
+the development version from <link
+xl:href="http://docbook.sourceforge.net/snapshots/docbook-xsl2-snapshot.zip"/>
+and unpack it somewhere. Then download and install Saxon 9 from <link
+xl:href="http://saxon.sf.net"/>.</para>
+
+<para>To transform a DocBook V5.0 document to a single HTML page use the command:</para>
+
+<screen><command>java</command> -jar <replaceable>/path/to/</replaceable>saxon9.jar -o output.html document.xml <replaceable>/path/to/</replaceable>docbook-xsl2-snapshot/html/docbook.xsl</screen>
+
+<para>To transform a DocBook V5.0 document to a set of chunked HTML pages use the command:</para>
+
+<screen><command>java</command> -jar <replaceable>/path/to/</replaceable>saxon9.jar document.xml <replaceable>/path/to/</replaceable>docbook-xsl2-snapshot/html/chunk.xsl</screen>
+
+</section>
+
+</section>
+
+</section>
+
+<section xml:id="changes">
+<title>Markup changes</title>
+
+<para>This section describes the most common markup changes
+between DocBook V4.x and V5.0.
+You can find a complete list of changes in
+<citation>DB5SPEC</citation>.</para>
+
+<section xml:id="changes-linking">
+<title>Improved cross-referencing and linking</title>
+
+<para>In DocBook V4.x the attribute <tag dbk:class="attribute">id</tag> is
+used to assign a unique identifier to an element. In DocBook V5.0 this
+attribute is renamed <tag dbk:class="attribute">xml:id</tag> in order
+to comply with <citation>XMLID</citation>.</para>
+
+<para>Now you can use almost any inline element as the source of a link,
+not just <tag>xref</tag> or <tag>link</tag>. For example, the following
+DocBook 4.x content:</para> 
+
+<programlisting><![CDATA[<section id="dir">
+  <title>DIR command</title>
+  <para>...</para>
+</section>
+
+<section id="ls">
+  <title>LS command</title>
+  <para>This command is a synonym for <link linkend="dir"><command>DIR</command></link> command.</para>
+</section>]]></programlisting>
+
+<para>is written in DocBook V5.0 as:</para>
+
+<programlisting><![CDATA[<section xml:id="dir">
+  <title>DIR command</title>
+  <para>...</para>
+</section>
+
+<section xml:id="ls">
+  <title>LS command</title>
+  <para>This command is a synonym for <command linkend="dir">DIR</command> command.</para>
+</section>]]></programlisting>
+
+<para>The <tag dbk:class="attribute">linkend</tag> attribute was added to all
+inline elements together with the <tag dbk:class="attribute">href</tag>
+attribute from the XLink namespace. This means that you can use any inline
+element as the source of a hypertext link. To use XLinks you have
+to declare the XLink namespace (most often on the root element of your
+document):</para>
+
+<programlisting><![CDATA[<article xmlns="http://docbook.org/ns/docbook" 
+         xmlns:xl="http://www.w3.org/1999/xlink" version="5.0">
+  <title>Test article</title>
+
+  <para><application xl:href="http://www.gnu.org/software/emacs/emacs.html">Emacs</application> 
+    is my favourite text editor.</para>]]>
+  …</programlisting>
+
+<para>The <tag dbk:condition="v4">ulink</tag> element was removed from DocBook V5.0
+in favor of XLink linking. Instead of the DocBook V4.x <tag dbk:condition="v4">ulink</tag>
+element:</para>
+
+<programlisting><![CDATA[<ulink url="http://docbook.org">DocBook site</ulink>]]></programlisting>
+
+<para>you can now use <tag>link</tag></para>
+
+<programlisting><![CDATA[<link xl:href="http://docbook.org">DocBook site</link>]]></programlisting>
+
+<para>XLink links may contain a fragment identifier, which you can 
+use instead of <tag dbk:class="attribute">linkend</tag> to form
+cross-references inside a document; for example:</para>
+
+<programlisting><![CDATA[<command xl:href="#dir">DIR</command>]]></programlisting>
+
+<para>However XLink links are not checked during validation, while <tag
+dbk:class="attribute">xml:id</tag>/<tag dbk:class="attribute">linkend</tag>
+links are checked for ID/IDREF consistency.
+One place where the XLink-based, fragment identifier scheme is
+useful is when XInclude is being used, since XML ID/IDREF links
+cannot span XInclude boundaries.
+You can use whichever approach better suits your needs.</para>
+</section>
+
+<section xml:id="changes-renamed">
+<title>Renamed elements</title>
+
+<para>Some elements were renamed to better express their meaning or to
+reduce the total number of elements available in DocBook.</para>
+
+<table xml:id="t.renamed">
+<title>Renamed elements</title>
+<tgroup dbk:cols="2">
+<thead>
+<row>
+<entry>Old name</entry>
+<entry>New name</entry>
+</row>
+</thead>
+<tbody>
+<row>
+<entry><tag dbk:condition="v4">sgmltag</tag></entry>
+<entry><tag>tag</tag></entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">bookinfo</tag>, <tag dbk:condition="v4">articleinfo</tag>,
+<tag dbk:condition="v4">chapterinfo</tag>, <tag dbk:condition="nolink">*info</tag></entry>
+<entry><tag>info</tag></entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">authorblurb</tag></entry>
+<entry><tag>personblurb</tag></entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">collabname</tag>, <tag dbk:condition="v4">corpauthor</tag>,
+<tag dbk:condition="v4">corpcredit</tag>, <tag dbk:condition="v4">corpname</tag></entry>
+<entry><tag>orgname</tag></entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">isbn</tag>, <tag dbk:condition="v4">issn</tag>,
+<tag dbk:condition="v4">pubsnumber</tag></entry>
+<entry><tag>biblioid</tag></entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">lot</tag>, <tag dbk:condition="v4">lotentry</tag>, <tag dbk:condition="v4">tocback</tag>,
+<tag dbk:condition="v4">tocchap</tag>, <tag dbk:condition="v4">tocfront</tag>, <tag dbk:condition="v4">toclevel1</tag>,
+<tag dbk:condition="v4">toclevel2</tag>, <tag dbk:condition="v4">toclevel3</tag>, <tag dbk:condition="v4">toclevel4</tag>,
+<tag dbk:condition="v4">toclevel5</tag>, <tag dbk:condition="v4">tocpart</tag></entry>
+<entry><tag>tocdiv</tag></entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">graphic</tag>, <tag dbk:condition="v4">graphicco</tag>,
+<tag dbk:condition="v4">inlinegraphic</tag>, <tag dbk:condition="v4">mediaobjectco</tag></entry>
+<entry><tag>mediaobject</tag> and <tag>inlinemediaobject</tag></entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">ulink</tag></entry>
+<entry><tag>link</tag></entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">ackno</tag></entry>
+<entry><tag>acknowledgements</tag></entry>
+</row>
+</tbody>
+</tgroup>
+</table>
+
+</section>
+
+<section xml:id="changes-removed">
+<title>Removed elements</title>
+
+<para>The following elements were removed from DocBook V5.0 without
+direct replacements: <tag dbk:condition="v4">action</tag>, <tag
+dbk:condition="v4">beginpage</tag>, <tag dbk:condition="v4">highlights</tag>,
+<tag dbk:condition="v4">interface</tag>, <tag
+dbk:condition="v4">invpartnumber</tag>, <tag
+dbk:condition="v4">medialabel</tag>, <tag dbk:condition="v4">modespec</tag>,
+<tag dbk:condition="v4">structfield</tag>, <tag
+dbk:condition="v4">structname</tag>.
+If you use one or more of these elements, here are some suggestions
+as to how to re-code them in DocBook V5.0.
+</para>
+
+<table xml:id="t.removed">
+<title>Recommended mapping for removed elements</title>
+<tgroup dbk:cols="2">
+<thead>
+<row>
+<entry>Old name</entry>
+<entry>Recommended mapping</entry>
+</row>
+</thead>
+<tbody>
+<row>
+<entry><tag dbk:condition="v4">action</tag></entry>
+<entry>Use <computeroutput>&lt;<tag>phrase</tag> remap="action"&gt;</computeroutput>.</entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">beginpage</tag></entry>
+<entry>Remove: <tag dbk:condition="v4">beginpage</tag> is advisory only
+and has tended to cause confusion.  A processing instruction or
+comment should be a workable replacement if one is needed.</entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">highlights</tag></entry>
+<entry>Use <tag>abstract</tag>.  Note that because <tag
+dbk:condition="v4">highlights</tag> has a broader content model, you may
+need to wrap contents in a <tag>para</tag> inside
+<tag>abstract</tag>.</entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">interface</tag></entry>
+<entry>Use one of the <quote>gui*</quote> elements
+(<tag>guibutton</tag>, <tag>guiicon</tag>, <tag>guilabel</tag>,
+<tag>guimenu</tag>, <tag>guimenuitem</tag>, or
+<tag>guisubmenu</tag>).</entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">invpartnumber</tag></entry>
+<entry>Use <computeroutput>&lt;<tag>biblioid</tag> class="other"
+otherclass="medialabel"&gt;</computeroutput>.  The
+<tag>productnumber</tag> element is another alternative.</entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">medialabel</tag></entry>
+<entry>Use <computeroutput>&lt;<tag>citetitle</tag>
+pubwork="<replaceable>mediatype</replaceable>"&gt;</computeroutput>,
+where <replaceable>mediatype</replaceable> is the type of media being
+labeled (e.g.,<tag dbk:class="attvalue">cdrom</tag> or <tag
+dbk:class="attvalue">dvd</tag>).</entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">modespec</tag></entry>
+<entry>No longer needed.  The current processing model for
+<tag>olink</tag> renders <tag dbk:condition="v4">modespec</tag>
+unnecessary.</entry>
+</row>
+<row>
+<entry><tag dbk:condition="v4">structfield</tag>, <tag dbk:condition="v4">structname</tag></entry>
+<entry>Use <tag>varname</tag>. If you need to distinguish between the
+two, use <computeroutput>&lt;<tag>varname</tag>
+remap="<replaceable>structname or
+structfield</replaceable>"&gt;</computeroutput>.  In some contexts, it
+may also be appropriate to use <tag>property</tag> for <tag
+dbk:condition="v4">structfield</tag>.</entry>
+</row>
+</tbody>
+</tgroup>
+</table>
+
+</section>
+
+</section>
+
+<section xml:id="convert4to5">
+<title>Converting DocBook V4.x documents to DocBook V5.0</title>
+
+<para>The DocBook V5.0 schema ships with an XSLT 1.0 stylesheet that
+is designed to transform valid DocBook V4.x documents to valid
+DocBook V5.0 documents.</para>
+
+<para>To convert your document, <filename>doc.xml</filename> in the
+examples below, follow these steps:</para>
+
+<procedure>
+<step>
+<para>Check the validity of your DocBook XML V4.x document. The
+conversion tool assumes that the input document is valid. If the input
+document contains markup errors, the results will be unpredictable at
+best.</para>
+</step>
+<step>
+<para>Transform <filename>doc.xml</filename> to
+<filename>newdoc.xml</filename> with the
+<filename>db4-upgrade.xsl</filename> stylesheet included in the
+DocBook V5.0 distribution that you are using.</para>
+</step>
+<step>
+<para>Check the validity of your DocBook XML V5.0 document against
+the DocBook V5.0 RELAX NG grammar.</para>
+</step>
+</procedure>
+
+<para>In the vast majority of cases, the resulting document should
+be valid and your conversion process is finished.</para>
+
+<para>If the document is not valid, please report the problem.
+(Over time, we'll have more experience with the sorts of things
+that can go wrong and we'll update this document to reflect that
+experience.)</para>
+
+<section xml:id="entities">
+<title>What About Entities?</title>
+
+<para>Using XSLT to transform existing documents to DocBook V5.0 has
+one potential disadvantage: it removes all entity references from 
+your document.</para>
+
+<para>If preserving entities is an important aspect of your production
+work flow, you will have to engage in a semi-manual process to
+preserve them.</para>
+
+<procedure>
+<step>
+<para>Open your existing document using your favorite editing tool.
+You must use a tool that <emphasis>is not</emphasis> XML-aware, or one
+that allows you to edit markup “in the raw”.</para>
+</step>
+<step>
+<para>Replace all occurrences of the entity references that you want
+to preserve with some unique string. For example, if you want to preserve
+“<literal>&amp;Product;</literal>” references, you could replace them
+all with “<literal>[[[Product]]]</literal>” (assuming that the string
+“<literal>[[[Product]]]</literal>” doesn't occur anywhere else in your document).</para>
+</step>
+<step>
+<para>Copy the document type declaration off of your document and save
+it some place. The document type declaration is everything from
+“<literal>&lt;!DOCTYPE</literal>” to the closing “<literal>]></literal>”.
+</para>
+</step>
+<step>
+<para>Perform the conversion described in <xref dbk:linkend="convert4to5"/>.
+</para>
+</step>
+<step>
+<para>Open the new document using your favorite editing tool. Replace
+all occurrences of the unique string you used to save the entity references
+with the corresponding entity references.</para>
+</step>
+<step>
+<para>Paste the document type declaration that you saved onto the top
+of your new document.</para>
+</step>
+<step>
+<para>Remove the external identifier (the <literal>PUBLIC</literal>
+and/or <literal>SYSTEM</literal> keywords) from the document type
+declaration. A document that begins:</para>
+<programlisting><![CDATA[<!DOCTYPE book [
+<!ENTITY someEntity "some replacement text">
+]>]]></programlisting>
+<para>is perfectly well-formed. If you don't remove the references to
+the DTD, then your parser will likely try to validate against DocBook
+V4.0 and that's not going to work. Alternatively, you could refer
+to the DocBook V5.0 DTD.</para>
+</step>
+</procedure>
+
+<tip>
+<para>Steps 2 and 5 from previous procedure can be automated using the
+<link xl:href="http://docbook.svn.sourceforge.net/viewvc/docbook/trunk/contrib/tools/cloak">cloak
+script</link> written by Michael Smith.</para>
+</tip>
+
+<section xml:id="extparsedentities">
+<title>External Parsed Entities</title>
+
+<para>External parsed entities, entities which load part of a document
+from another file, are a special case. These can often be replaced
+with XInclude elements.</para>
+
+<para>The Perl script <filename>db4-entities.pl</filename>, also included
+in the DocBook V5.0 distribution attempts to perform this replacement
+for you. To use the script, perform the following steps:</para>
+
+<procedure>
+<step>
+<para>Process your document with <filename>db4-entities.pl</filename>.
+The script expects a single filename and prints the XInclude version
+on standard output.</para>
+</step>
+<step>
+<para>Process the XInclude version as described in <xref
+dbk:linkend="convert4to5"/>.
+</para>
+</step>
+</procedure>
+</section>
+</section>
+
+</section>
+
+<section xml:id="customizations">
+  <title>Customizing DocBook V5.0</title>
+  <!--
+      ** RNG schema organization
+      ** Removing attributes
+      ** Adding new attributes
+      ** Changing permitted content of attribute
+      ** Removing elements
+      ** Adding new elements
+      ** Customizing content models
+      ** Naming and versioning of DocBook customizations
+  -->
+
+  <para>
+    It's much easier to customize DocBook V5.0 than it was to
+    customize earlier releases.  This is partly because RELAX NG
+    provides better support for modifications than DTDs and partly
+    because the DocBook schema is designed to take full advantage
+    of the capabilities RELAX NG provides.
+    This section describes the organization of the RELAX NG schema for
+    DocBook, methods and examples for adding, removing, and modifying elements
+    and attributes, and conventions for naming and versioning
+    DocBook customizations.
+    It assumes some familiarity with RELAX NG.  If you are unfamiliar
+    with RELAX NG, you can find a tutorial introduction in
+    <citation>RNCTUT</citation>.
+  </para>
+  <section xml:id="relaxngorg">
+    <title>DocBook RELAX NG schema organization</title>
+    <para>
+      The DocBook RELAX NG schema is highly modular, using named
+      patterns extensively.  Every element, attribute, attribute
+      list, and enumeration has its own named pattern.  In addition,
+      there are named patterns for logical combinations of elements
+      and attributes.  These named patterns provide <quote>hooks</quote>
+      into the schema that allow you to do a wide range of customization
+      by simply redefining one or more of the named patterns.
+    </para>
+    <para>
+      An important design characteristic of the schema is that
+      duplication is minimized.  This is done through the use of
+      named patterns for common groupings that can be re-used.
+      For example, the <tag>imagedata</tag> and <tag>videodata</tag>
+      elements each have an <tag dbk:class="attribute">align</tag> attribute
+      that takes the same set of enumerated values.  Rather than
+      repeating those values, a single pattern,
+      <varname>db.halign.enumeration</varname> is referenced by
+      the <varname>db.videodata.align.enumeration</varname>
+      and <varname>db.imagedata.align.enumeration</varname> patterns,
+      which are in turn referenced by the
+      <varname>db.videodata.align.attribute</varname>
+      and <varname>db.imagedata.align.attribute</varname> patterns.
+      While this may seem like overkill, it allows a customizer to modify
+      the allowed enumerations for these two attributes separately or together,
+      or to completely re-define the allowed content of either or both,
+      by redefining one or more of these named patterns.
+    </para>
+    <section xml:id="patternnames"><title>Pattern Names</title>
+    <para>
+      Because named patterns are used extensively, the RELAX NG schema uses
+      several naming conventions.  These are:
+      <itemizedlist dbk:spacing="compact">
+        <listitem>
+          <para>
+            Names have two or more parts, separated by dots <quote>.</quote>
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            The first part of each name is the prefix <quote>db</quote>
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            Each element has a named pattern in the form
+            <varname>db.<replaceable>elementname</replaceable></varname>.
+            Elements that have different content models in different
+            contexts will also have patterns in the form
+            <varname>db.<replaceable>context.elementname</replaceable></varname>.  For example, <varname>db.figure.info</varname>
+            defines the content model for the <tag>info</tag> element
+            when it appears as a child of the <tag>figure</tag> element.
+            <replaceable>Context</replaceable> may have several parts.
+            For example, <varname>db.cals.entrytbl.thead</varname>.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            Most attributes have a named pattern in the form
+            <varname>db.<replaceable>attributename</replaceable>.attribute</varname>.
+            Attributes that have different content models in different
+            contexts will also have patterns in the form
+            <varname>db.<replaceable>context.attributename</replaceable>.attribute</varname>.
+            For example,
+            <varname>db.olink.localinfo.attribute</varname> defines the content
+            model of the <tag dbk:class="attribute">localinfo</tag> attribute when
+            it appears in <tag>olink</tag>.
+            There are a few attributes that do not have individual named
+            patterns.  For example, the effectivity attributes are grouped
+            into <varname>db.effectivity.attributes</varname> and not identified
+            separately.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            Each element has a named pattern for its attribute list in
+            the form
+            <varname>db.<replaceable>elementname</replaceable>.attlist</varname>
+
+            that defines the list of attributes for that element.
+            Elements that have different attribute lists in different
+            contexts will also have patterns in the form
+            <varname>db.<replaceable>context.elementname</replaceable>.attlist</varname>
+            For example, <varname>db.html.table.attlist</varname> defines
+            the attribute list for the html <tag dbk:condition="nolink">table</tag> element and
+            <varname>db.cals.table.attlist</varname> defines the attribute
+            list for a cals <tag dbk:condition="nolink">table</tag> element.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            Each attribute that has enumerated values has a
+            named pattern in the form
+            <varname>db.<replaceable>[context.]attributename</replaceable>.enumeration</varname>.
+            If the enumeration for a particular attribute depends on
+            context, optional context is provided.
+            For example,
+            <varname>db.verbatim.continuation.enumeration</varname> defines
+            the enumeration values for the
+            <tag dbk:class="attribute">continuation</tag> attribute that is used
+            in verbatim contexts like <tag>screen</tag>.
+            Unlike elements and attributes, there is not necessarily a
+            named pattern for enumerated attributes outside their context.
+            For example, there is no <varname>db.class.enumeration</varname>
+            because the <tag dbk:class="attribute">class</tag> attribute has
+            a broad and non-intersecting range of uses.
+          </para>
+        </listitem>
+        <listitem>
+          <para>
+            There are several different groupings of elements and attributes.
+            Here are the major ones:
+            <variablelist dbk:spacing="compact">
+              <varlistentry>
+                <term>inlines</term>
+                <listitem>
+                  <para>
+                    Combinations of inline elements, for example,
+                    <varname>db.error.inlines</varname>, which contains
+                    <varname>db.errorcode</varname>,
+                    <varname>db.errortext</varname>, etc.
+                  </para>
+                </listitem>
+              </varlistentry>
+              <varlistentry>
+                <term>blocks</term>
+                <listitem>
+                  <para>
+                    Combinations of block elements, for example,
+                    <varname>db.verbatim.blocks</varname>, which contains
+                    <varname>db.programlisting</varname>,
+                    <varname>db.screen</varname>, etc.
+                  </para>
+                </listitem>
+              </varlistentry>
+              <varlistentry>
+                <term>attributes</term>
+                <listitem>
+                  <para>
+                    Combinations of attributes, for example,
+                    <varname>db.effectivity.attributes</varname>,
+                    which contains the attributes
+                    <tag dbk:class="attribute">arch</tag>,
+                    <tag dbk:class="attribute">condition</tag>,
+                    <tag dbk:class="attribute">conformance</tag>, etc.
+                  </para>
+                </listitem>
+              </varlistentry>
+              <varlistentry>
+                <term>components</term>
+                <listitem>
+                  <para>
+                    High level components of the schema, for example,
+                    <varname>db.navigation.components</varname>, which contains
+                    <varname>db.glossary</varname>,
+                    <varname>db.bibliography</varname>,
+                    <varname>db.index</varname>, and
+                    <varname>db.toc</varname>, and is used inside the
+                    content model for <tag>chapter</tag>, <tag>appendix</tag>,
+                    and <tag>preface</tag>.
+                  </para>
+                </listitem>
+              </varlistentry>
+              <varlistentry>
+                <term>contentmodel</term>
+                <listitem>
+                  <para>
+                    Shared content models, for example,
+                    <varname>db.admonition.contentmodel</varname>, which contains
+                    the content model for <tag>tip</tag>, <tag>warning</tag>,
+                    <tag>note</tag>, etc.
+                  </para>
+                </listitem>
+              </varlistentry>
+            </variablelist>
+          </para>
+          <para>
+            There are a couple of other groupings designed to minimize
+            duplication, but these are the most important.
+          </para>
+        </listitem>
+      </itemizedlist>
+    </para>
+  </section>
+</section>
+<section xml:id="customconsiderations">
+  <title>General customization considerations</title>
+  <para>
+    Creating a customized schema is similar to
+    creating a customization layer for XSL.  The schema customization
+    layer is a new RELAX NG schema that defines your changes and
+    includes the standard docbook schema.  You then validate using
+    the schema customization as your schema.
+  </para>
+  <para>
+    <xref dbk:linkend="ex-empty" dbk:xrefstyle="select: label"/> is an empty
+    RELAX NG customization that does nothing
+    except define the name spaces and include the standard DocBook schema.
+    The <tag dbk:class="attribute">href</tag> attribute of the
+    <tag dbk:condition="nolink">include</tag> element points to
+    the location of the standard DocBook V5.0
+    schema.<footnote><para>The examples in this section use
+    <filename>docbook.rng</filename> as the schema location. If you want
+    to create a portable schema customization you should use a standard
+    web-accessible location like
+    <uri>http://docbook.org/xml/&version;/rng/docbook.rng</uri> and
+    then use <link
+    xl:href="http://www.oasis-open.org/committees/download.php/14809/xml-catalogs.html">XML
+    catalogs</link> to resolve this location to your local copy of the
+    schema for improved performance. Unfortunately, at the time of
+    this writing not all RELAX NG validators support XML catalogs.</para></footnote>
+    All of the examples are given in both RNG and RNC form.
+<example xml:id="ex-empty"><title>Empty customization file</title>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns:db="http://docbook.org/ns/docbook"
+         ns="http://docbook.org/ns/docbook"
+         xmlns="http://relaxng.org/ns/structure/1.0">
+  <include href="docbook.rng"/>
+
+  <!-- redefinitions of named patterns -->
+
+</grammar>]]></programlisting>
+<programlisting dbk:language="rnc"><![CDATA[namespace db = "http://docbook.org/ns/docbook"
+
+include "docbook.rnc" inherit = db
+# redefinitions of named patterns]]></programlisting>
+</example>
+  </para>
+</section>
+  <section xml:id="cust-elements">
+    <title>Elements</title>
+    <section xml:id="cust-add-elements">
+      <title>Adding elements</title>
+      <para>
+        Adding an element typically takes two definitions.
+        The first defines the new element and
+        its content model, and the second adds the
+        new element into the schema.  We'll show two examples.
+      </para>
+      <para>
+        <xref dbk:linkend="ex-add-element-1"  dbk:xrefstyle="select: label"/>
+        adds a new element,
+        <tag dbk:condition="nolink">person</tag>, with the same
+        content model as <tag>author</tag>.  The new element will be
+        allowed to appear wherever <tag>author</tag> can appear.
+      </para>
+      <para>
+        The <varname>db.author</varname> pattern is copied
+        and renamed <varname>dbx.person</varname>, defining
+        a new element called <tag dbk:condition="nolink">person</tag>.
+        Then, the <varname>db.author</varname> pattern is redefined
+        to be a choice of the current value or <varname>dbx.person</varname>.
+        The <tag dbk:class="attribute">combine</tag> attribute tells
+        RELAX NG to combine this pattern with the existing named
+        pattern.  In this case, the value
+        of the <tag dbk:class="attribute">combine</tag> attribute is
+        <quote>choice</quote>, which tells the parser that either
+        the original pattern or this new pattern is a valid match.
+      </para>
+<example xml:id="ex-add-element-1"><title>Adding a new element by duplicating an existing one</title>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns:db="http://docbook.org/ns/docbook"
+         ns="http://docbook.org/ns/docbook"
+         xmlns="http://relaxng.org/ns/structure/1.0">
+  <include href="docbook.rng"/>
+  <!-- define the new element -->
+  <define name="dbx.person">
+    <element name="person">
+        <ref name="db.author.attlist"/>
+        <ref name="db.credit.contentmodel"/>
+    </element>
+  </define>
+  <!-- redefine the db.author pattern to allow db.person in
+       the same places as db.author -->
+  <define name="db.author" combine="choice">
+    <ref name="dbx.person"/>
+  </define>
+</grammar>]]></programlisting>
+<programlisting dbk:language="rnc"><![CDATA[default namespace db = "http://docbook.org/ns/docbook"
+
+include "docbook.rnc"
+# define the new element
+dbx.person =
+  element person { db.author.attlist, db.credit.contentmodel }
+# redefine the db.author pattern to allow db.person in
+# the same places as db.author
+db.author |= dbx.person]]></programlisting>
+</example>
+    <para>
+      The preceding method works well when you'd like a new element
+      to be a clone or near-clone of an existing element.  It gives
+      you complete control over the content model, but
+      only limited control over where the element is allowed.  It
+      works well when you want to allow the element in the same places
+      as an existing element, and for this example that works
+      nicely, since <tag>author</tag> is allowed in four different
+      named patterns, each of which would have had to be redefined to
+      allow <tag dbk:condition="nolink">person</tag>.
+      But, if you can't find an existing element that is allowed in
+      exactly the places you need, this method doesn't work as well.
+    </para>
+    <para>
+      <xref dbk:linkend="ex-add-element-2" dbk:xrefstyle="select: label"/>
+      adds two new elements by combining them into
+      a higher level pattern.  In this example, we'll add
+      two new inline elements for writing about assembly language,
+      <tag dbk:condition="nolink">register</tag> and 
+      <tag dbk:condition="nolink">instruction</tag>.
+      We will allow them wherever programming inlines
+      or operating system inlines are allowed.
+      <xref dbk:linkend="ex-add-element-2" dbk:xrefstyle="select: label"/>
+      defines the two elements, creates a new named pattern
+      (<varname>dbx.asm.inlines</varname>) that contains them, and adds
+      that pattern to <varname>db.programming.inlines</varname> and
+      <varname>db.os.inlines</varname>.  Since these two patterns
+      don't have any elements in common, the strategy used in 
+      <xref dbk:linkend="ex-add-element-1" dbk:xrefstyle="select: label"/>
+      would require selecting two different elements to <quote>clone</quote>,
+      which would be messy.
+    </para>
+<example xml:id="ex-add-element-2"><title>Adding new inline elements</title>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns:db="http://docbook.org/ns/docbook"
+         ns="http://docbook.org/ns/docbook"
+         xmlns="http://relaxng.org/ns/structure/1.0">
+  <include href="docbook.rng"/>
+  <!-- define the new elements -->
+  <define name="dbx.register">
+    <element name="register">
+      <text/>
+    </element>
+  </define>
+  <define name="dbx.instruction">
+    <element name="instruction">
+      <text/>
+    </element>
+  </define>
+  <!-- create a new pattern that contains the new inlines -->
+  <define name="dbx.asm.inlines">
+    <choice>
+      <ref name="dbx.register"/>
+      <ref name="dbx.instruction"/>
+    </choice>
+  </define>
+  <!-- add the new inlines to programming and os inlines -->
+    <define name="db.programming.inlines" combine="choice">
+      <ref name="dbx.asm.inlines"/>
+    </define>
+    <define name="db.os.inlines" combine="choice">
+      <ref name="dbx.asm.inlines"/>
+    </define>
+</grammar>]]></programlisting>
+<programlisting dbk:language="rnc"><![CDATA[default namespace db = "http://docbook.org/ns/docbook"
+
+include "docbook.rnc"
+# define the new elements
+dbx.register = element register { text }
+dbx.instruction = element instruction { text }
+# create a new pattern that contains the new inlines
+dbx.asm.inlines = dbx.register | dbx.instruction
+# add the new inlines to programming and os inlines
+db.programming.inlines |= dbx.asm.inlines
+db.os.inlines |= dbx.asm.inlines]]></programlisting>
+</example>
+    </section>
+    <section xml:id="cust-delete-elements">
+      <title>Deleting elements</title>
+      <para>
+        Deleting elements is straightforward, but takes some
+        care and planning.  <xref dbk:linkend="ex-delete-element"
+        dbk:xrefstyle="select: label"/> deletes
+        the <tag>important</tag> admonition element by redefining
+        it with a content model of <varname>notAllowed</varname>.
+        Note that in this example, the redefinition is inside
+        the <tag dbk:condition="nolink">include</tag> element.
+        This is required for
+        redefinitions that completely replace an existing pattern.
+      </para>
+      <para>
+        Be careful; If you delete an element that is a required part
+        of another element's content model, you can make it
+        impossible to create a valid document.
+        For example, if you delete the <tag>title</tag>
+        element, you won't be able to validate a <tag>book</tag>
+        because a <tag>book</tag> requires a <tag>title</tag>.
+      </para>
+<example xml:id="ex-delete-element"><title>Deleting an element</title>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns:db="http://docbook.org/ns/docbook"
+         ns="http://docbook.org/ns/docbook"
+         xmlns="http://relaxng.org/ns/structure/1.0">
+  <include href="docbook.rng">
+    <!-- redefine important element as notAllowed -->
+    <define name="db.important">
+      <notAllowed/>
+    </define>
+  </include>
+</grammar>]]></programlisting>
+<programlisting dbk:language="rnc"><![CDATA[namespace db = "http://docbook.org/ns/docbook"
+
+include "docbook.rnc" inherit = db {
+  # redefine important element as notAllowed
+  db.important = notAllowed
+}]]></programlisting>
+</example>
+    </section>
+    <section xml:id="cust-modify-elements">
+      <title>Customizing the content model of existing elements</title>
+      <para>
+         <xref dbk:linkend="ex-modify-element" dbk:xrefstyle="select: label"/>
+         expands the definition of <tag>author</tag> to include two
+         new elements, <tag dbk:condition="nolink">born</tag> and
+         <tag dbk:condition="nolink">died</tag>.
+         The <tag>author</tag> element allows two content models,
+         <varname>db.person.author.contentmodel</varname>, which
+         defines an author who is a person, and
+         <varname>db.org.author.contentmodel</varname>, which
+         defines an author that is an organization.  We will modify
+         <varname>db.person.author.contentmodel</varname> so that
+         only authors who are persons can have the new elements.
+<example xml:id="ex-modify-element"><title>Modifying the content model of an element</title>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns:db="http://docbook.org/ns/docbook"
+         ns="http://docbook.org/ns/docbook"
+         xmlns="http://relaxng.org/ns/structure/1.0">
+  <include href="docbook.rng"/>
+
+  <define name="db.person.author.contentmodel" combine="interleave">
+    <interleave>
+      <optional>
+        <element name="born">
+          <ref name="db.date.contentmodel"/>
+        </element>
+      </optional>
+      <optional>
+        <element name="died">
+          <ref name="db.date.contentmodel"/>
+        </element>
+      </optional>
+    </interleave>
+  </define>
+</grammar>]]></programlisting>
+<programlisting dbk:language="rnc"><![CDATA[default namespace = "http://docbook.org/ns/docbook"
+namespace db = "http://docbook.org/ns/docbook"
+
+include "docbook.rnc"
+
+db.person.author.contentmodel &=
+  element born { db.date.contentmodel }?
+  & element died { db.date.contentmodel }?]]></programlisting>
+</example>
+      </para>
+      <para>
+        This modification will allow instances like this:
+<programlisting><![CDATA[<author>
+  <personname>Babe Ruth</personname>
+  <born>02/06/1895</born>
+  <died>08/16/1948</died>
+</author>]]></programlisting>
+but because we only modified the content model for authors
+who are human, it won't allow an instance like this, which
+uses <varname>db.org.author.contentmodel</varname>:
+<programlisting><![CDATA[<!-- INVALID -->
+<author>
+  <orgname>Boston Red Sox</orgname>
+  <died>1919</died>
+  <born>2004</born>
+</author>]]></programlisting>
+      </para>
+    </section>
+  </section>
+  <section xml:id="cust-attributes">
+    <title>Attributes</title>
+    <section xml:id="cust-add-attributes">
+      <title>Adding attributes</title>
+      <para>
+        The simplest way to add an attribute to a single element
+        is to add it to the attlist pattern for that element.
+        <xref dbk:linkend="ex-add-attr" dbk:xrefstyle="select: label"/>
+        adds the optional attributes <tag dbk:class="attribute">born</tag>
+        and <tag dbk:class="attribute">died</tag> to the attribute
+        list for <tag>author</tag>.
+        The <varname>db.author.attlist</varname>
+        named pattern is redefined with the
+        <tag dbk:class="attribute">combine</tag> attribute set to
+        <quote>interleave</quote>, which interleaves the two new
+        optional attributes with the existing attributes on the list.
+      </para>
+<example xml:id="ex-add-attr"><title>Adding attributes</title>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns:db="http://docbook.org/ns/docbook"
+         ns="http://docbook.org/ns/docbook"
+         xmlns="http://relaxng.org/ns/structure/1.0">
+  <include href="docbook.rng"/>
+
+  <define name="db.author.attlist" combine="interleave">
+    <interleave>
+      <optional>
+        <attribute name="born">
+          <ref name="db.date.contentmodel"/>
+        </attribute>
+      </optional>
+      <optional>
+        <attribute name="died">
+          <ref name="db.date.contentmodel"/>
+        </attribute>
+      </optional>
+    </interleave>
+  </define>
+</grammar>]]></programlisting>
+<programlisting dbk:language="rnc"><![CDATA[namespace db = "http://docbook.org/ns/docbook"
+
+include "docbook.rnc" inherit = db
+
+db.author.attlist &=
+  attribute born { db.date.contentmodel }?
+  & attribute died { db.date.contentmodel }?]]></programlisting>
+</example>
+    <para>
+      Unlike
+      <xref dbk:linkend="ex-modify-element" dbk:xrefstyle="select: label"/>,
+      <xref dbk:linkend="ex-add-attr" dbk:xrefstyle="select: label"/> allows
+      the new attributes to appear on any <tag>author</tag>
+      element, not just those using the person content model.
+    </para>
+    <para>
+      <xref dbk:linkend="ex-add-attr-2" dbk:xrefstyle="select: label"/> shows
+      how you could limit the use of these attributes to authors who
+      are persons.  In this example, the new attributes are interleaved
+      with the <varname>db.person.author.contentmodel</varname>.  
+      The only difference between this example and 
+      <xref dbk:linkend="ex-modify-element" dbk:xrefstyle="select: label"/> is
+      that the added patterns are identified as attributes rather than
+      elements.  This shows some of the flexibility of RELAX NG, which
+      treats attributes and elements very consistently.
+<example xml:id="ex-add-attr-2"><title>Adding attributes; alternate method</title>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns:db="http://docbook.org/ns/docbook"
+         ns="http://docbook.org/ns/docbook"
+         xmlns="http://relaxng.org/ns/structure/1.0">
+  <include href="docbook.rng"/>
+  <!-- redefinitions of named patterns -->
+  <define name="db.person.author.contentmodel" combine="interleave">
+    <interleave>
+      <optional>
+        <attribute name="born">
+          <ref name="db.date.contentmodel"/>
+        </attribute>
+      </optional>
+      <optional>
+        <attribute name="died">
+          <ref name="db.date.contentmodel"/>
+        </attribute>
+      </optional>
+    </interleave>
+  </define>
+</grammar>]]></programlisting>
+<programlisting dbk:language="rnc"><![CDATA[namespace db = "http://docbook.org/ns/docbook"
+
+include "docbook.rnc" inherit = db
+# redefinitions of named patterns
+db.person.author.contentmodel &=
+  attribute born { db.date.contentmodel }?
+  & attribute died { db.date.contentmodel }?]]></programlisting>
+</example>
+There is one difference in the treatment of attributes and elements
+that is worth noting.  By the XML 1.0 definition, the relative order
+of attributes is not significant.  Therefore, the
+<tag dbk:condition="nolink">interleave</tag> block is not required for
+attributes, though it does no harm.  
+    </para>
+    </section>
+    <section xml:id="cust-delete-attributes">
+      <title>Deleting attributes</title>
+      <para>
+        Deleting an attribute is similar to deleting an element,
+        except that you use the RELAX NG <varname>empty</varname>
+        pattern rather than <varname>notAllowed</varname>.
+        <xref dbk:linkend="ex-delete-attr" dbk:xrefstyle="select: label"/>
+        deletes the linking attributes, which are collected in the
+        <varname>db.common.linking.attributes</varname> pattern,
+        by defining that pattern as <varname>empty</varname>.
+      </para>
+<example xml:id="ex-delete-attr"><title>Deleting an attribute</title>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns:db="http://docbook.org/ns/docbook"
+         ns="http://docbook.org/ns/docbook"
+         xmlns="http://relaxng.org/ns/structure/1.0">
+  <include href="docbook.rng">
+    <define name="db.common.linking.attributes">
+      <empty/>
+    </define>
+  </include>
+</grammar>]]></programlisting>
+<programlisting dbk:language="rnc"><![CDATA[namespace db = "http://docbook.org/ns/docbook"
+
+include "docbook.rnc" inherit = db {
+  db.common.linking.attributes = empty
+}]]></programlisting>
+</example>
+      <para>
+        Generally, <varname>empty</varname> is used when deleting
+        attributes and <varname>notAllowed</varname> is used when
+        deleting elements.
+      </para>
+    </section>
+    <section xml:id="cust-modify-attributes">
+      <title>Changing permitted content of attributes</title>
+      <para>
+        <xref dbk:linkend="ex-modify-attr" dbk:xrefstyle="select: label"/>
+        modifies <varname>db.spacing.enumeration</varname> to
+        add the additional value <quote>large</quote>.  Note
+        that to remove a value from an enumeration, you need
+        to redefine the entire enumeration, minus the values
+        you don't need.
+      </para>
+<example xml:id="ex-modify-attr"><title>Deleting an attribute</title>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns:db="http://docbook.org/ns/docbook"
+         ns="http://docbook.org/ns/docbook"
+         xmlns="http://relaxng.org/ns/structure/1.0">
+  <include href="docbook.rng"/>
+  <!-- add value to an enumeration -->
+  <define name="db.spacing.enumeration" combine="choice">
+    <value>large</value>
+  </define>
+</grammar>]]></programlisting>
+<programlisting dbk:language="rnc"><![CDATA[namespace db = "http://docbook.org/ns/docbook"
+
+include "docbook.rnc" inherit = db
+# add value to an enumeration
+db.spacing.enumeration |= "large"]]></programlisting>
+</example>
+    </section>
+  </section>
+
+<section xml:id="cust-naming">
+<title>Naming and versioning DocBook customizations</title>
+
+<para>DocBook V5.0 is not tightly coupled with some particular
+validation technology like DTDs. This also means that DocBook V5.0
+documents don't have to (and usually don't) start with a
+document type declaration (&lt;!DOCTYPE…>) to specify the schema
+(DTD) to use. Instead, DocBook V5.0 instances can be easily
+distinguished from other XML vocabularies by using elements in the
+<uri>http://docbook.org/ns/docbook</uri> namespace. This namespace is
+enough to distinguish DocBook from other XML based formats. But the
+DocBook schema evolves over time and there are several versions of
+DocBook (e.g. 3.1, 4.2, 4.5 and 5.0).  Since DocBook version 5.0, the
+actual version used is indicated in the <tag
+dbk:class="attribute">version</tag> attribute on a root element.</para>
+
+<programlisting><![CDATA[<book xmlns="http://docbook.org/ns/docbook"
+      version="5.0">
+  …
+</book>]]></programlisting>
+
+<para>Future versions of DocBook documents will start with the same
+markup, except the version number will be raised, for example to 5.1
+or 6.0.
+The namespace will remain the same until the semantics of the elements
+change in a backward incompatible way, which is very unlikely to happen.</para>
+
+<para>If you create a DocBook schema customization you must change the <tag
+dbk:class="attribute">version</tag> attribute to distinguish your
+customization from the <quote>official</quote> DocBook.  Changing the
+namespace is not recommended because that would break the processing
+tools.  Remember that changing namespaces is the same as renaming all
+elements in the namespace.</para>
+
+<para>When you customize the schema, use the following syntax to
+identify your DocBook derivation:</para>
+
+<programlisting><replaceable>base_version</replaceable>-[subset|extension|variant] [<replaceable>name</replaceable>[-<replaceable>version</replaceable>]?]+</programlisting>
+
+<para>For example:</para>
+
+<programlisting>5.0-subset simplified-1.0
+5.0-variant ASMBook
+5.0-variant ASMBook-2006
+5.0-extension MathML-2.0 SVG-1.1</programlisting>
+
+<para>The first part of the version identifier is the version number of the
+DocBook schema from which you derived your customization.</para>
+
+<para>If your schema is a proper subset, you can advertise this status
+by using the <literal>subset</literal> keyword in the description. If
+your schema contains any markup model extensions, you can advertise
+this status by using the <literal>extension</literal> keyword. If
+you'd rather not characterize your variant specifically as a subset or
+an extension, use the <literal>variant</literal> keyword.</para>
+
+<para>After these keywords you may add a whitespace separated list of
+customization identifiers. Each name may be optionally followed by its
+version number.</para>
+
+</section>
+
+</section>
+
+<section xml:id="faq">
+<title>FAQ</title>
+
+<qandaset>
+<qandadiv>
+<title>Authoring</title>
+
+<qandaentry xml:id="faq-authoring-schema-association">
+<question>
+<para>How do I attach a schema to a DocBook V5.0 document when I do not
+want to use DTDs and !DOCTYPE?</para>
+</question>
+<answer>
+<para>There is no standard way of associating a RELAX NG schema with a
+document. Most tools provide some mechanism for performing this
+association, consult the documentation for your application. In some
+tools you must specify schema manually each time you want to
+edit/process your document.</para>
+</answer>
+</qandaentry>
+
+<qandaentry xml:id="faq-authoring-general-entities">
+<question>
+<para>How do I use entities like <tag dbk:class="genentity">ndash</tag> in
+DocBook V5.0?</para>
+</question>
+<answer>
+<para>Modern schema languages (including RELAX NG and W3X XML Schema)
+do not provide any means to define entities that can be used for easier
+typing of special characters. Some editors provide functions or
+special toolbars that allow you to easily pick necessary character
+and insert it into document as a raw Unicode character or a numeric
+character reference.</para>
+<para>Another possibility is to include entity definitions in the
+prolog of your document. <link
+xl:href="http://www.w3.org/2003/entities/">Entity definition
+files</link> are now maintained by W3C. You can reference definition
+files with entity definitions you are interested in and then reference
+imported entities. For example:</para>
+<programlisting><![CDATA[<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE article [
+<!ENTITY % isopub SYSTEM "http://www.w3.org/2003/entities/iso8879/isopub.ent">
+%isopub;
+]>
+<article xmlns="http://docbook.org/ns/docbook" version="5.0">
+<title>DocBook V5.0 &ndash; the superb documentation format</title>]]>
+…</programlisting>
+<para>For your convenience there is also flattened entity definition
+file which contains all entity definitions.</para>
+<programlisting><![CDATA[<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE article [
+<!ENTITY % allent SYSTEM "http://www.w3.org/2003/entities/2007/w3centities-f.ent">
+%allent;
+]>
+<article xmlns="http://docbook.org/ns/docbook" version="5.0">
+<title>DocBook V5.0 &ndash; the superb documentation format</title>]]>
+…</programlisting>
+</answer>
+</qandaentry>
+
+<qandaentry xml:id="faq-authoring-modularization">
+<question>
+<para>How to modularize documents?</para>
+</question>
+<answer>
+<para>You can use <link
+xl:href="http://www.w3.org/TR/xinclude/">XInclude</link> for this
+task. There is an alternative schema for DocBook V5.0 that
+contains XInclude elements. This is necessary to make some XML editors
+happy. This schema can be found in files that end with letters <quote>xi</quote>, e.g.
+<filename>docbookxi.rnc</filename> instead of
+<filename>docbook.rnc</filename>.</para>
+</answer>
+</qandaentry>
+
+<qandaentry xml:id="faq-authoring-validating-xincludes">
+<question>
+<para>How to validate documents which are composed by XInclude?</para>
+</question>
+<answer>
+<para>If you are using XIncludes you should make sure that the final
+document after resolving all inclusions is valid DocBook V5.0
+instance. This means that all XIncludes should be processed before
+validation takes place. The following command can be used to enable
+XInclude processing in oNVDL.</para>
+<screen><command>java</command> -Dorg.apache.xerces.xni.parser.XMLParserConfiguration=org.apache.xerces.parsers.XIncludeParserConfiguration -jar <replaceable>/path/to/oNVDL/</replaceable>bin/onvdl.jar <replaceable>/path/to/</replaceable>docbook.nvdl document.xml</screen>
+<para>For JNVDL you can use switch <option>-xi</option> to enable XInclude processing.</para>
+</answer>
+</qandaentry>
+
+</qandadiv>
+
+<qandadiv>
+<title>Stylesheets</title>
+
+<qandaentry xml:id="faq-stylesheets-future">
+<question>
+<para>Will the current DocBook XSL stylesheets (XSLT 1.0 based
+implementation) be maintained and improved in the future since work on
+a new XSLT 2.0 based implementation has started?</para>
+</question>
+<answer>
+<para>Yes, the current stylesheets (like 1.73.x) will be supported and
+improved further because they are very widely deployed and work with
+many existing XSLT processors.</para>
+<para>Surely there will be a point in a future when all new development
+will be switched to the XSLT 2.0 based implementation. But this
+will not happen until all features of the current stylesheets are
+implemented in the new stylesheets, and until there is more than
+one usable XSLT 2.0 processor available.</para>
+</answer>
+</qandaentry>
+
+</qandadiv>
+
+<qandadiv>
+<title>Schema customizations</title>
+
+<qandaentry xml:id="faq-customization-mathml">
+<question>
+<para>How can I extend the DocBook schema with MathML elements?</para>
+</question>
+<answer>
+<para>The basic DocBook schema allows elements from the MathML namespace
+to appear inside the <tag>equation</tag> element.  This means that you can
+validate a DocBook+MathML document, but MathML content will be ignored
+during the validation. You will also not be able to use guided editing
+for the MathML content.</para>
+<para>If you need strict validation of MathML content or guided
+editing for MathML, you can easily extend the base DocBook schema with
+the MathML schema.</para>
+<procedure>
+<title>Extending the DocBook schema with the MathML schema</title>
+<step>
+<para>Download the MathML RELAX NG schema from <link
+xl:href="http://yupotan.sppd.ne.jp/relax-ng/mml2.html"/> and unpack it
+somewhere (e.g. into a <filename>mathml</filename> subdirectory).</para>
+</step>
+<step>
+<para>Create a schema customization in compact syntax—<filename>dbmathml.rnc</filename>:</para>
+<programlisting dbk:language="rnc">namespace html = "http://www.w3.org/1999/xhtml"
+namespace mml = "http://www.w3.org/1998/Math/MathML"
+namespace db = "http://docbook.org/ns/docbook"
+
+include "/path/to/docbook.rnc" {
+  db._any.mml = external "mathml/mathml2.rnc"
+  db._any =
+    element * - (db:* | html:* | mml:*) {
+      (attribute * { text }
+       | text
+       | db._any)*
+    }
+}</programlisting>
+<para>Or, alternatively, you can use the XML syntax of RELAX NG—<filename>dbmathml.rng</filename>:</para>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+
+<include href="/path/to/docbook.rng">
+  <define name="db._any.mml">
+    <externalRef href="mathml/mathml2.rng"/>
+  </define>
+
+  <define name="db._any">
+    <element>
+      <anyName>
+        <except>
+          <nsName ns="http://docbook.org/ns/docbook"/>
+          <nsName ns="http://www.w3.org/1999/xhtml"/>
+          <nsName ns="http://www.w3.org/1998/Math/MathML"/>
+        </except>
+      </anyName>
+      <zeroOrMore>
+        <choice>
+          <attribute>
+            <anyName/>
+          </attribute>
+          <text/>
+          <ref name="db._any"/>
+        </choice>
+      </zeroOrMore>
+    </element>
+  </define>
+</include>
+
+</grammar>]]></programlisting>
+</step>
+<step>
+<para>Now use the customized schema (<filename>dbmathml.rnc</filename>
+or <filename>dbmathml.rng</filename>) instead of the original
+DocBook schema.</para>
+</step>
+</procedure>
+</answer>
+</qandaentry>
+
+<qandaentry xml:id="faq-customization-svg">
+<question>
+<para>How can I extend the DocBook schema with SVG elements?</para>
+</question>
+<answer>
+<para>The situation is the same as with MathML support. You can use
+elements from the SVG namespace inside the <tag>imageobject</tag>
+element.</para>
+<procedure>
+<title>Extending the DocBook schema with the SVG schema</title>
+<step>
+<para>Download the SVG RELAX NG schema from <link
+xl:href="http://www.w3.org/Graphics/SVG/1.1/rng/rng.zip"/> and unpack it
+somewhere (e.g. into an <filename>svg</filename> subdirectory).</para>
+</step>
+<step>
+<para>Create a schema customization in compact syntax—<filename>dbsvg.rnc</filename>:</para>
+<programlisting dbk:language="rnc">namespace html = "http://www.w3.org/1999/xhtml"
+namespace db = "http://docbook.org/ns/docbook"
+namespace svg = "http://www.w3.org/2000/svg"
+
+include "/path/to/docbook.rnc" {
+  db._any.svg = external "svg/svg11.rnc"
+  db._any =
+    element * - (db:* | html:* | svg:*) {
+      (attribute * { text }
+       | text
+       | db._any)*
+    }
+}</programlisting>
+<para>Or, alternatively, you can use the XML syntax of RELAX NG—<filename>dbsvg.rng</filename>:</para>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+
+<include href="/path/to/docbook.rng">
+  <define name="db._any.svg">
+    <externalRef href="svg/svg11.rng"/>
+  </define>
+
+  <define name="db._any">
+    <element>
+      <anyName>
+        <except>
+          <nsName ns="http://docbook.org/ns/docbook"/>
+          <nsName ns="http://www.w3.org/1999/xhtml"/>
+          <nsName ns="http://www.w3.org/2000/svg"/>
+        </except>
+      </anyName>
+      <zeroOrMore>
+        <choice>
+          <attribute>
+            <anyName/>
+          </attribute>
+          <text/>
+          <ref name="db._any"/>
+        </choice>
+      </zeroOrMore>
+    </element>
+  </define>
+</include>
+
+</grammar>]]></programlisting>
+</step>
+<step>
+<para>Now use the customized schema (<filename>dbsvg.rnc</filename>
+or <filename>dbsvg.rng</filename>) instead of the original
+DocBook schema.</para>
+</step>
+</procedure>
+</answer>
+</qandaentry>
+
+<qandaentry xml:id="faq-customization-mathml-svg">
+<question>
+<para>Is it possible to use the previous two customizations for MathML
+and SVG together?</para>
+</question>
+<answer>
+<para>Yes, you can create a special schema customization that combines
+both MathML and SVG with the DocBook schema. In compact syntax, the merged
+schema is:</para>
+<programlisting dbk:language="rnc">namespace html = "http://www.w3.org/1999/xhtml"
+namespace mml = "http://www.w3.org/1998/Math/MathML"
+namespace db = "http://docbook.org/ns/docbook"
+namespace svg = "http://www.w3.org/2000/svg"
+
+include "/path/to/docbook.rnc" {
+  db._any.mml = external "mahtml/mathml2.rnc"
+  db._any.svg = external "svg/svg11.rnc"
+  db._any =
+    element * - (db:* | html:* | mml:* | svg:*) {
+      (attribute * { text }
+       | text
+       | db._any)*
+    }
+}</programlisting>
+<para>Or alternatively in the full RELAX NG syntax:</para>
+<programlisting dbk:language="rng"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
+<grammar xmlns="http://relaxng.org/ns/structure/1.0">
+
+<include href="/path/to/docbook.rng">
+  <define name="db._any.mml">
+    <externalRef href="mathml/mathml2.rng"/>
+  </define>
+
+  <define name="db._any.svg">
+    <externalRef href="svg/svg11.rng"/>
+  </define>
+
+  <define name="db._any">
+    <element>
+      <anyName>
+        <except>
+          <nsName ns="http://docbook.org/ns/docbook"/>
+          <nsName ns="http://www.w3.org/1999/xhtml"/>
+          <nsName ns="http://www.w3.org/1998/Math/MathML"/>
+          <nsName ns="http://www.w3.org/2000/svg"/>
+        </except>
+      </anyName>
+      <zeroOrMore>
+        <choice>
+          <attribute>
+            <anyName/>
+          </attribute>
+          <text/>
+          <ref name="db._any"/>
+        </choice>
+      </zeroOrMore>
+    </element>
+  </define>
+</include>
+
+</grammar>]]></programlisting>
+</answer>
+</qandaentry>
+
+<qandaentry xml:id="faq-customization-links">
+<question>
+<para>Are there any other examples of schema customization
+available?</para>
+</question>
+<answer>
+<para>Sure. Some of the are listed bellow:</para>
+<itemizedlist>
+<listitem><para><link
+xl:href="http://www.w3.org/TR/xml-i18n-bp/#docbook-plus-its">Sample
+customization of ITS and DocBook</link></para></listitem>
+<listitem><para><link
+xl:href="http://wiki.docbook.org/topic/DocbookSchemas">Examples on
+DocBook WiKi</link></para></listitem>
+</itemizedlist>
+</answer>
+</qandaentry>
+
+</qandadiv>
+
+<qandadiv>
+<title>Tool specific problems</title>
+
+<qandaentry xml:id="faq-tools-xmlspy-xmlid">
+<question>
+<para>I'm using Altova XMLSpy to validate DocBook V5.0 instances
+against the W3C XML Schema (<filename>docbook.xsd</filename>). XMLSpy
+complains about undefined <tag dbk:class="attribute">xml:id</tag>
+attributes?</para>
+</question>
+<answer>
+<para>XMLSpy always uses its own bundled version of
+<filename>xml.xsd</filename> which unfortunately doesn't define the <tag
+dbk:class="attribute">xml:id</tag> attribute. The bundled version of
+<filename>xml.xsd</filename> is hardwired into the program and cannot
+be replaced by a newer version. To solve this problem you must upgrade
+to version 2006 SP1.</para>
+</answer>
+</qandaentry>
+
+</qandadiv>
+
+</qandaset>
+</section>
+
+<bibliography xml:id="references">
+
+<bibliomixed>
+<abbrev>RNCTUT</abbrev>
+Clark, James – Cowan, John – MURATA, Makoto: <title>RELAX NG Compact Syntax Tutorial</title>.
+Working Draft, 26 March 2003. OASIS. <bibliomisc><link xl:href="http://relaxng.org/compact-tutorial-20030326.html"/></bibliomisc>
+</bibliomixed>
+
+<bibliomixed>
+<abbrev>NVDLTUT</abbrev>
+Nálevka, Petr:
+<title>NVDL Tutorial</title>.
+<bibliomisc><link xl:href="http://jnvdl.sourceforge.net/tutorial.html"/></bibliomisc>
+</bibliomixed>
+
+<bibliomixed>
+<abbrev>XMLID</abbrev>
+Marsh, Jonathan – 
+Veillard, Daniel –
+Walsh, Norman: <title>xml:id Version 1.0</title>. W3C Recommendation, 9 September 2005. <bibliomisc><link xl:href="http://www.w3.org/TR/xml-id/"/></bibliomisc>
+</bibliomixed>
+
+<bibliomixed>
+<abbrev>DB5SPEC</abbrev>
+Norman, Walsh: <title>The DocBook Schema</title>.
+Working Draft 5.0a1, OASIS, 29 June 2005.
+<bibliomisc><link xl:href="http://www.docbook.org/specs/wd-docbook-docbook-5.0a1.html"/></bibliomisc>
+</bibliomixed>
+
+</bibliography>
+</article>
+</book>
diff --git a/org.argeo.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java b/org.argeo.jcr/ext/test/org/argeo/jcr/fs/JcrFileSystemTest.java
new file mode 100644 (file)
index 0000000..e24a7de
--- /dev/null
@@ -0,0 +1,129 @@
+package org.argeo.jcr.fs;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Arrays;
+import java.util.Map;
+
+import javax.jcr.Property;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.jackrabbit.fs.JackrabbitMemoryFsProvider;
+
+import junit.framework.TestCase;
+
+public class JcrFileSystemTest extends TestCase {
+       private final static Log log = LogFactory.getLog(JcrFileSystemTest.class);
+
+       public void testSimple() throws Exception {
+               FileSystemProvider fsProvider = new JackrabbitMemoryFsProvider();
+
+               // Simple file
+               Path rootPath = fsProvider.getPath(new URI("jcr+memory:/"));
+               log.debug("Got root " + rootPath);
+               Path testPath = fsProvider.getPath(new URI("jcr+memory:/test.txt"));
+               log.debug("Test path");
+               assertEquals("test.txt", testPath.getFileName().toString());
+               assertEquals(rootPath, testPath.getParent());
+               assertEquals(testPath.getFileName(), rootPath.relativize(testPath));
+               // relativize self should be empty path
+               Path selfRelative = testPath.relativize(testPath);
+               assertEquals("", selfRelative.toString());
+
+               log.debug("Create file " + testPath);
+               Files.createFile(testPath);
+               BasicFileAttributes bfa = Files.readAttributes(testPath, BasicFileAttributes.class);
+               FileTime ft = bfa.creationTime();
+               assertNotNull(ft);
+               assertTrue(bfa.isRegularFile());
+               log.debug("Created " + testPath + " (" + ft + ")");
+               Files.delete(testPath);
+               log.debug("Deleted " + testPath);
+               String txt = "TEST\nTEST2\n";
+               byte[] arr = txt.getBytes();
+               Files.write(testPath, arr);
+               log.debug("Wrote " + testPath);
+               byte[] read = Files.readAllBytes(testPath);
+               assertTrue(Arrays.equals(arr, read));
+               assertEquals(txt, new String(read));
+               log.debug("Read " + testPath);
+               Path testDir = rootPath.resolve("testDir");
+               log.debug("Resolved " + testDir);
+               // Copy
+               Files.createDirectory(testDir);
+               log.debug("Created directory " + testDir);
+               Path subsubdir = Files.createDirectories(testDir.resolve("subdir/subsubdir"));
+               log.debug("Created sub directories " + subsubdir);
+               Path copiedFile = testDir.resolve("copiedFile.txt");
+               log.debug("Resolved " + copiedFile);
+               Path relativeCopiedFile = testDir.relativize(copiedFile);
+               assertEquals(copiedFile.getFileName().toString(), relativeCopiedFile.toString());
+               log.debug("Relative copied file " + relativeCopiedFile);
+               try (OutputStream out = Files.newOutputStream(copiedFile); InputStream in = Files.newInputStream(testPath)) {
+                       IOUtils.copy(in, out);
+               }
+               log.debug("Copied " + testPath + " to " + copiedFile);
+               Files.delete(testPath);
+               log.debug("Deleted " + testPath);
+               byte[] copiedRead = Files.readAllBytes(copiedFile);
+               assertTrue(Arrays.equals(copiedRead, read));
+               log.debug("Read " + copiedFile);
+               // Browse directories
+               DirectoryStream<Path> files = Files.newDirectoryStream(testDir);
+               int fileCount = 0;
+               Path listedFile = null;
+               for (Path file : files) {
+                       fileCount++;
+                       if (!Files.isDirectory(file))
+                               listedFile = file;
+               }
+               assertEquals(2, fileCount);
+               assertEquals(copiedFile, listedFile);
+               assertEquals(copiedFile.toString(), listedFile.toString());
+               log.debug("Listed " + testDir);
+               // Generic attributes
+               Map<String, Object> attrs = Files.readAttributes(copiedFile, "*");
+               assertEquals(5, attrs.size());
+               log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet());
+               // Direct node access
+               NodeFileAttributes nfa = Files.readAttributes(copiedFile, NodeFileAttributes.class);
+               nfa.getNode().addMixin(NodeType.MIX_LANGUAGE);
+               nfa.getNode().getSession().save();
+               log.debug("Add mix:language");
+               Files.setAttribute(copiedFile, Property.JCR_LANGUAGE, "fr");
+               log.debug("Set language");
+               attrs = Files.readAttributes(copiedFile, "*");
+               assertEquals(6, attrs.size());
+               log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet());
+       }
+
+       public void testIllegalCharacters() throws Exception {
+               FileSystemProvider fsProvider = new JackrabbitMemoryFsProvider();
+               String fileName = "tüßçt[1].txt";
+               String pathStr = "/testDir/" + fileName;
+               Path testDir = fsProvider.getPath(new URI("jcr+memory:/testDir"));
+               Files.createDirectory(testDir);
+               Path testPath = testDir.resolve(fileName);
+               assertEquals(pathStr, testPath.toString());
+               Files.createFile(testPath);
+               DirectoryStream<Path> files = Files.newDirectoryStream(testDir);
+               Path listedPath = files.iterator().next();
+               assertEquals(pathStr, listedPath.toString());
+
+               String dirName = "*[~WeirdDir~]*";
+               Path subDir = testDir.resolve(dirName);
+               Files.createDirectory(subDir);
+               subDir = testDir.resolve(dirName);
+               assertEquals(dirName, subDir.getFileName().toString());
+       }
+}
diff --git a/org.argeo.jcr/ext/test/org/argeo/server/jcr/JcrResourceAdapterTest.java b/org.argeo.jcr/ext/test/org/argeo/server/jcr/JcrResourceAdapterTest.java
new file mode 100644 (file)
index 0000000..11dc4fa
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.server.jcr;
+
+import java.io.InputStream;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.List;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.jackrabbit.unit.AbstractJackrabbitTestCase;
+import org.argeo.jcr.JcrResourceAdapter;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+
+@Deprecated
+public class JcrResourceAdapterTest extends AbstractJackrabbitTestCase {
+       private static SimpleDateFormat sdf = new SimpleDateFormat(
+                       "yyyyMMdd:hhmmss.SSS");
+
+       private final static Log log = LogFactory
+                       .getLog(JcrResourceAdapterTest.class);
+
+       private JcrResourceAdapter jra;
+
+       public void testCreate() throws Exception {
+               String basePath = "/test/subdir";
+               jra.mkdirs(basePath);
+               Resource res = new ClassPathResource("org/argeo/server/jcr/dummy00.xls");
+               String filePath = basePath + "/dummy.xml";
+               jra.create(filePath, res.getInputStream(), "application/vnd.ms-excel");
+               InputStream in = jra.retrieve(filePath);
+               assertTrue(IOUtils.contentEquals(res.getInputStream(), in));
+       }
+
+       public void testVersioning() throws Exception {
+               String basePath = "/test/versions";
+               jra.mkdirs(basePath);
+               String filePath = basePath + "/dummy.xml";
+               Resource res00 = new ClassPathResource(
+                               "org/argeo/server/jcr/dummy00.xls");
+               jra.create(filePath, res00.getInputStream(), "application/vnd.ms-excel");
+               Resource res01 = new ClassPathResource(
+                               "org/argeo/server/jcr/dummy01.xls");
+               jra.update(filePath, res01.getInputStream());
+               Resource res02 = new ClassPathResource(
+                               "org/argeo/server/jcr/dummy02.xls");
+               jra.update(filePath, res02.getInputStream());
+
+               List<Calendar> versions = jra.listVersions(filePath);
+               log.debug("Versions of " + filePath);
+               int count = 0;
+               for (Calendar version : versions) {
+                       log.debug(" " + (count == 0 ? "base" : count - 1) + "\t"
+                                       + sdf.format(version.getTime()));
+                       count++;
+               }
+
+               assertEquals(4, versions.size());
+
+               InputStream in = jra.retrieve(filePath, 1);
+               assertTrue(IOUtils.contentEquals(res01.getInputStream(), in));
+               in = jra.retrieve(filePath, 0);
+               assertTrue(IOUtils.contentEquals(res00.getInputStream(), in));
+               in = jra.retrieve(filePath, 2);
+               assertTrue(IOUtils.contentEquals(res02.getInputStream(), in));
+               Resource res03 = new ClassPathResource(
+                               "org/argeo/server/jcr/dummy03.xls");
+               jra.update(filePath, res03.getInputStream());
+               in = jra.retrieve(filePath, 1);
+               assertTrue(IOUtils.contentEquals(res01.getInputStream(), in));
+       }
+
+       @Override
+       protected void setUp() throws Exception {
+               log.debug("SET UP");
+               super.setUp();
+               jra = new JcrResourceAdapter();
+               jra.setSession(session());
+       }
+
+       @Override
+       protected void tearDown() throws Exception {
+               log.debug("TEAR DOWN");
+               super.tearDown();
+       }
+}
diff --git a/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy00.xls b/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy00.xls
new file mode 100644 (file)
index 0000000..e5846fe
Binary files /dev/null and b/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy00.xls differ
diff --git a/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy01.xls b/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy01.xls
new file mode 100644 (file)
index 0000000..b5c6b55
Binary files /dev/null and b/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy01.xls differ
diff --git a/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy02.xls b/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy02.xls
new file mode 100644 (file)
index 0000000..d73bc66
Binary files /dev/null and b/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy02.xls differ
diff --git a/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy03.xls b/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy03.xls
new file mode 100644 (file)
index 0000000..0759cb9
Binary files /dev/null and b/org.argeo.jcr/ext/test/org/argeo/server/jcr/dummy03.xls differ
diff --git a/org.argeo.jcr/pom.xml b/org.argeo.jcr/pom.xml
new file mode 100644 (file)
index 0000000..4ad3c57
--- /dev/null
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.jcr</artifactId>
+       <name>Commons JCR</name>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.util</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.jcr/repository.xml b/org.argeo.jcr/repository.xml
new file mode 100644 (file)
index 0000000..745079e
--- /dev/null
@@ -0,0 +1,152 @@
+<?xml version="1.0"?>\r
+<!--\r
+   Licensed to the Apache Software Foundation (ASF) under one or more\r
+   contributor license agreements.  See the NOTICE file distributed with\r
+   this work for additional information regarding copyright ownership.\r
+   The ASF licenses this file to You under the Apache License, Version 2.0\r
+   (the "License"); you may not use this file except in compliance with\r
+   the License.  You may obtain a copy of the License at\r
+\r
+       http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+   Unless required by applicable law or agreed to in writing, software\r
+   distributed under the License is distributed on an "AS IS" BASIS,\r
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+   See the License for the specific language governing permissions and\r
+   limitations under the License.\r
+-->\r
+\r
+<!DOCTYPE Repository\r
+          PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 2.0//EN"\r
+          "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">\r
+\r
+<!-- Example Repository Configuration File\r
+     Used by\r
+     - org.apache.jackrabbit.core.config.RepositoryConfigTest.java\r
+     -\r
+-->\r
+<Repository>\r
+    <!--\r
+        virtual file system where the repository stores global state\r
+        (e.g. registered namespaces, custom node types, etc.)\r
+    -->\r
+    <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">\r
+        <param name="path" value="${rep.home}/repository"/>\r
+    </FileSystem>\r
+\r
+    <!--\r
+        data store configuration\r
+    -->\r
+    <DataStore class="org.apache.jackrabbit.core.data.FileDataStore"/>\r
+\r
+    <!--\r
+        security configuration\r
+    -->\r
+    <Security appName="Jackrabbit">\r
+        <!--\r
+            security manager:\r
+            class: FQN of class implementing the JackrabbitSecurityManager interface\r
+        -->\r
+        <SecurityManager class="org.apache.jackrabbit.core.DefaultSecurityManager" workspaceName="security">\r
+            <!--\r
+            workspace access:\r
+            class: FQN of class implementing the WorkspaceAccessManager interface\r
+            -->\r
+            <!-- <WorkspaceAccessManager class="..."/> -->\r
+            <!-- <param name="config" value="${rep.home}/security.xml"/> -->\r
+        </SecurityManager>\r
+\r
+        <!--\r
+            access manager:\r
+            class: FQN of class implementing the AccessManager interface\r
+        -->\r
+        <AccessManager class="org.apache.jackrabbit.core.security.DefaultAccessManager">\r
+            <!-- <param name="config" value="${rep.home}/access.xml"/> -->\r
+        </AccessManager>\r
+\r
+        <LoginModule class="org.apache.jackrabbit.core.security.authentication.DefaultLoginModule">\r
+           <!-- \r
+              anonymous user name ('anonymous' is the default value)\r
+            -->\r
+           <param name="anonymousId" value="anonymous"/>\r
+           <!--\r
+              administrator user id (default value if param is missing is 'admin')\r
+            -->\r
+           <param name="adminId" value="admin"/>\r
+        </LoginModule>\r
+    </Security>\r
+\r
+    <!--\r
+        location of workspaces root directory and name of default workspace\r
+    -->\r
+    <Workspaces rootPath="${rep.home}/workspaces" defaultWorkspace="default"/>\r
+    <!--\r
+        workspace configuration template:\r
+        used to create the initial workspace if there's no workspace yet\r
+    -->\r
+    <Workspace name="${wsp.name}">\r
+        <!--\r
+            virtual file system of the workspace:\r
+            class: FQN of class implementing the FileSystem interface\r
+        -->\r
+        <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">\r
+            <param name="path" value="${wsp.home}"/>\r
+        </FileSystem>\r
+        <!--\r
+            persistence manager of the workspace:\r
+            class: FQN of class implementing the PersistenceManager interface\r
+        -->\r
+        <PersistenceManager class="org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager">\r
+          <param name="url" value="jdbc:derby:${wsp.home}/db;create=true"/>\r
+          <param name="schemaObjectPrefix" value="${wsp.name}_"/>\r
+        </PersistenceManager>\r
+        <!--\r
+            Search index and the file system it uses.\r
+            class: FQN of class implementing the QueryHandler interface\r
+        -->\r
+        <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">\r
+            <param name="path" value="${wsp.home}/index"/>\r
+            <param name="supportHighlighting" value="true"/>\r
+        </SearchIndex>\r
+    </Workspace>\r
+\r
+    <!--\r
+        Configures the versioning\r
+    -->\r
+    <Versioning rootPath="${rep.home}/version">\r
+        <!--\r
+            Configures the filesystem to use for versioning for the respective\r
+            persistence manager\r
+        -->\r
+        <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">\r
+            <param name="path" value="${rep.home}/version" />\r
+        </FileSystem>\r
+\r
+        <!--\r
+            Configures the persistence manager to be used for persisting version state.\r
+            Please note that the current versioning implementation is based on\r
+            a 'normal' persistence manager, but this could change in future\r
+            implementations.\r
+        -->\r
+        <PersistenceManager class="org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager">\r
+          <param name="url" value="jdbc:derby:${rep.home}/version/db;create=true"/>\r
+          <param name="schemaObjectPrefix" value="version_"/>\r
+        </PersistenceManager>\r
+    </Versioning>\r
+\r
+    <!--\r
+        Search index for content that is shared repository wide\r
+        (/jcr:system tree, contains mainly versions)\r
+    -->\r
+    <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">\r
+        <param name="path" value="${rep.home}/repository/index"/>\r
+        <param name="supportHighlighting" value="true"/>\r
+    </SearchIndex>\r
+\r
+    <!--\r
+        Run with a cluster journal\r
+    -->\r
+    <Cluster id="node1">\r
+        <Journal class="org.apache.jackrabbit.core.journal.MemoryJournal"/>\r
+    </Cluster>\r
+</Repository>\r
diff --git a/org.argeo.jcr/repository/repository/meta/rootUUID b/org.argeo.jcr/repository/repository/meta/rootUUID
new file mode 100644 (file)
index 0000000..df09293
--- /dev/null
@@ -0,0 +1 @@
+cafebabe-cafe-babe-cafe-babecafebabe
\ No newline at end of file
diff --git a/org.argeo.jcr/repository/repository/namespaces/ns_idx.properties b/org.argeo.jcr/repository/repository/namespaces/ns_idx.properties
new file mode 100644 (file)
index 0000000..7e757f0
--- /dev/null
@@ -0,0 +1,8 @@
+#Fri Oct 28 20:14:30 CEST 2016
+http\://www.jcp.org/jcr/1.0=1570322
+internal=16762557
+http\://www.jcp.org/jcr/sv/1.0=16463688
+http\://www.jcp.org/jcr/mix/1.0=14361695
+http\://www.jcp.org/jcr/nt/1.0=5688619
+.empty.key=0
+http\://www.w3.org/XML/1998/namespace=6829023
diff --git a/org.argeo.jcr/repository/repository/namespaces/ns_reg.properties b/org.argeo.jcr/repository/repository/namespaces/ns_reg.properties
new file mode 100644 (file)
index 0000000..f40bf21
--- /dev/null
@@ -0,0 +1,8 @@
+#Fri Oct 28 20:14:30 CEST 2016
+jcr=http\://www.jcp.org/jcr/1.0
+sv=http\://www.jcp.org/jcr/sv/1.0
+xml=http\://www.w3.org/XML/1998/namespace
+nt=http\://www.jcp.org/jcr/nt/1.0
+mix=http\://www.jcp.org/jcr/mix/1.0
+rep=internal
+.empty.key=
diff --git a/org.argeo.jcr/repository/workspaces/default/workspace.xml b/org.argeo.jcr/repository/workspaces/default/workspace.xml
new file mode 100644 (file)
index 0000000..a32f9c7
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?><Workspace name="default">
+        <!--
+            virtual file system of the workspace:
+            class: FQN of class implementing the FileSystem interface
+        -->
+        <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+            <param name="path" value="${wsp.home}"/>
+        </FileSystem>
+        <!--
+            persistence manager of the workspace:
+            class: FQN of class implementing the PersistenceManager interface
+        -->
+        <PersistenceManager class="org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager">
+          <param name="url" value="jdbc:derby:${wsp.home}/db;create=true"/>
+          <param name="schemaObjectPrefix" value="${wsp.name}_"/>
+        </PersistenceManager>
+        <!--
+            Search index and the file system it uses.
+            class: FQN of class implementing the QueryHandler interface
+        -->
+        <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+            <param name="path" value="${wsp.home}/index"/>
+            <param name="supportHighlighting" value="true"/>
+        </SearchIndex>
+    </Workspace>
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java b/org.argeo.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java
new file mode 100644 (file)
index 0000000..7396c87
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.jackrabbit;
+
+import java.util.Map;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+import org.apache.jackrabbit.core.security.SecurityConstants;
+import org.apache.jackrabbit.core.security.principal.AdminPrincipal;
+
+@Deprecated
+public class JackrabbitAdminLoginModule implements LoginModule {
+       private Subject subject;
+
+       @Override
+       public void initialize(Subject subject, CallbackHandler callbackHandler,
+                       Map<String, ?> sharedState, Map<String, ?> options) {
+               this.subject = subject;
+       }
+
+       @Override
+       public boolean login() throws LoginException {
+               // TODO check permission?
+               return true;
+       }
+
+       @Override
+       public boolean commit() throws LoginException {
+               subject.getPrincipals().add(
+                               new AdminPrincipal(SecurityConstants.ADMIN_ID));
+               return true;
+       }
+
+       @Override
+       public boolean abort() throws LoginException {
+               return true;
+       }
+
+       @Override
+       public boolean logout() throws LoginException {
+               subject.getPrincipals().removeAll(
+                               subject.getPrincipals(AdminPrincipal.class));
+               return true;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java b/org.argeo.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java
new file mode 100644 (file)
index 0000000..8fedcf5
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jackrabbit;
+
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+import javax.jcr.Session;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.argeo.jcr.ArgeoJcrException;
+import org.argeo.jcr.JcrCallback;
+import org.argeo.jcr.JcrUtils;
+import org.springframework.core.io.Resource;
+
+/** Migrate the data in a Jackrabbit repository. */
+@Deprecated
+public class JackrabbitDataModelMigration implements
+               Comparable<JackrabbitDataModelMigration> {
+       private final static Log log = LogFactory
+                       .getLog(JackrabbitDataModelMigration.class);
+
+       private String dataModelNodePath;
+       private String targetVersion;
+       private Resource migrationCnd;
+       private JcrCallback dataModification;
+
+       /**
+        * Expects an already started repository with the old data model to migrate.
+        * Expects to be run with admin rights (Repository.login() will be used).
+        * 
+        * @return true if a migration was performed and the repository needs to be
+        *         restarted and its caches cleared.
+        */
+       public Boolean migrate(Session session) {
+               long begin = System.currentTimeMillis();
+               Reader reader = null;
+               try {
+                       // check if already migrated
+                       if (!session.itemExists(dataModelNodePath)) {
+                               log.warn("Node " + dataModelNodePath
+                                               + " does not exist: nothing to migrate.");
+                               return false;
+                       }
+//                     Node dataModelNode = session.getNode(dataModelNodePath);
+//                     if (dataModelNode.hasProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION)) {
+//                             String currentVersion = dataModelNode.getProperty(
+//                                             ArgeoNames.ARGEO_DATA_MODEL_VERSION).getString();
+//                             if (compareVersions(currentVersion, targetVersion) >= 0) {
+//                                     log.info("Data model at version " + currentVersion
+//                                                     + ", no need to migrate.");
+//                                     return false;
+//                             }
+//                     }
+
+                       // apply transitional CND
+                       if (migrationCnd != null) {
+                               reader = new InputStreamReader(migrationCnd.getInputStream());
+                               CndImporter.registerNodeTypes(reader, session, true);
+                               session.save();
+                               log.info("Registered migration node types from " + migrationCnd);
+                       }
+
+                       // modify data
+                       dataModification.execute(session);
+
+                       // apply changes
+                       session.save();
+
+                       long duration = System.currentTimeMillis() - begin;
+                       log.info("Migration of data model " + dataModelNodePath + " to "
+                                       + targetVersion + " performed in " + duration + "ms");
+                       return true;
+               } catch (Exception e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new ArgeoJcrException("Migration of data model "
+                                       + dataModelNodePath + " to " + targetVersion + " failed.",
+                                       e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+                       IOUtils.closeQuietly(reader);
+               }
+       }
+
+       protected static int compareVersions(String version1, String version2) {
+               // TODO do a proper version analysis and comparison
+               return version1.compareTo(version2);
+       }
+
+       /** To be called on a stopped repository. */
+       public static void clearRepositoryCaches(RepositoryConfig repositoryConfig) {
+               try {
+                       String customeNodeTypesPath = "/nodetypes/custom_nodetypes.xml";
+                       // FIXME causes weird error in Eclipse
+                       //repositoryConfig.getFileSystem().deleteFile(customeNodeTypesPath);
+                       if (log.isDebugEnabled())
+                               log.debug("Cleared " + customeNodeTypesPath);
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot clear caches", e);
+               }
+
+               // File customNodeTypes = new File(home.getPath()
+               // + "/repository/nodetypes/custom_nodetypes.xml");
+               // if (customNodeTypes.exists()) {
+               // customNodeTypes.delete();
+               // if (log.isDebugEnabled())
+               // log.debug("Cleared " + customNodeTypes);
+               // } else {
+               // log.warn("File " + customNodeTypes + " not found.");
+               // }
+       }
+
+       /*
+        * FOR USE IN (SORTED) SETS
+        */
+
+       public int compareTo(JackrabbitDataModelMigration dataModelMigration) {
+               // TODO make ordering smarter
+               if (dataModelNodePath.equals(dataModelMigration.dataModelNodePath))
+                       return compareVersions(targetVersion,
+                                       dataModelMigration.targetVersion);
+               else
+                       return dataModelNodePath
+                                       .compareTo(dataModelMigration.dataModelNodePath);
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (!(obj instanceof JackrabbitDataModelMigration))
+                       return false;
+               JackrabbitDataModelMigration dataModelMigration = (JackrabbitDataModelMigration) obj;
+               return dataModelNodePath.equals(dataModelMigration.dataModelNodePath)
+                               && targetVersion.equals(dataModelMigration.targetVersion);
+       }
+
+       @Override
+       public int hashCode() {
+               return targetVersion.hashCode();
+       }
+
+       public void setDataModelNodePath(String dataModelNodePath) {
+               this.dataModelNodePath = dataModelNodePath;
+       }
+
+       public void setTargetVersion(String targetVersion) {
+               this.targetVersion = targetVersion;
+       }
+
+       public void setMigrationCnd(Resource migrationCnd) {
+               this.migrationCnd = migrationCnd;
+       }
+
+       public void setDataModification(JcrCallback dataModification) {
+               this.dataModification = dataModification;
+       }
+
+       public String getDataModelNodePath() {
+               return dataModelNodePath;
+       }
+
+       public String getTargetVersion() {
+               return targetVersion;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/JackrabbitRepositoryFactory.java b/org.argeo.jcr/src/org/argeo/jackrabbit/JackrabbitRepositoryFactory.java
new file mode 100644 (file)
index 0000000..f7e882e
--- /dev/null
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jackrabbit;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.jackrabbit.commons.JcrUtils;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.apache.jackrabbit.core.config.RepositoryConfigurationParser;
+import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory;
+import org.argeo.jcr.ArgeoJcrException;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.xml.sax.InputSource;
+
+/**
+ * Repository factory which can create new repositories and access remote
+ * Jackrabbit repositories
+ */
+@Deprecated
+public class JackrabbitRepositoryFactory implements RepositoryFactory {
+       // FIXME factorize with node
+       /** Key for a JCR repository alias */
+       public final static String JCR_REPOSITORY_ALIAS = "argeo.jcr.repository.alias";
+       /** Key for a JCR repository URI */
+       public final static String JCR_REPOSITORY_URI = "argeo.jcr.repository.uri";
+
+       private final static Log log = LogFactory
+                       .getLog(JackrabbitRepositoryFactory.class);
+
+       private Resource fileRepositoryConfiguration = new ClassPathResource(
+                       "/org/argeo/jackrabbit/repository-h2.xml");
+
+       @SuppressWarnings({ "rawtypes" })
+       public Repository getRepository(Map parameters) throws RepositoryException {
+               // // check if can be found by alias
+               // Repository repository = super.getRepository(parameters);
+               // if (repository != null)
+               // return repository;
+
+               // check if remote
+               Repository repository;
+               String uri = null;
+               if (parameters.containsKey(JCR_REPOSITORY_URI))
+                       uri = parameters.get(JCR_REPOSITORY_URI).toString();
+               else if (parameters.containsKey(JcrUtils.REPOSITORY_URI))
+                       uri = parameters.get(JcrUtils.REPOSITORY_URI).toString();
+
+               if (uri != null) {
+                       if (uri.startsWith("http"))// http, https
+                               repository = createRemoteRepository(uri);
+                       else if (uri.startsWith("file"))// http, https
+                               repository = createFileRepository(uri, parameters);
+                       else if (uri.startsWith("vm")) {
+                               log.warn("URI "
+                                               + uri
+                                               + " should have been managed by generic JCR repository factory");
+                               repository = getRepositoryByAlias(getAliasFromURI(uri));
+                       } else
+                               throw new ArgeoJcrException("Unrecognized URI format " + uri);
+
+               }
+
+               else if (parameters.containsKey(JCR_REPOSITORY_ALIAS)) {
+                       // Properties properties = new Properties();
+                       // properties.putAll(parameters);
+                       String alias = parameters.get(JCR_REPOSITORY_ALIAS).toString();
+                       // publish(alias, repository, properties);
+                       // log.info("Registered JCR repository under alias '" + alias + "'
+                       // with properties " + properties);
+                       repository = getRepositoryByAlias(alias);
+               } else
+                       throw new ArgeoJcrException("Not enough information in "
+                                       + parameters);
+
+               if (repository == null)
+                       throw new ArgeoJcrException("Repository not found " + parameters);
+
+               return repository;
+       }
+
+       protected Repository getRepositoryByAlias(String alias) {
+               return null;
+       }
+
+       protected Repository createRemoteRepository(String uri)
+                       throws RepositoryException {
+               Map<String, String> params = new HashMap<String, String>();
+               params.put(JcrUtils.REPOSITORY_URI, uri);
+               Repository repository = new Jcr2davRepositoryFactory()
+                               .getRepository(params);
+               if (repository == null)
+                       throw new ArgeoJcrException("Remote Davex repository " + uri
+                                       + " not found");
+               log.info("Initialized remote Jackrabbit repository from uri " + uri);
+               return repository;
+       }
+
+       @SuppressWarnings({ "rawtypes", "unchecked" })
+       protected Repository createFileRepository(final String uri, Map parameters)
+                       throws RepositoryException {
+               InputStream configurationIn = null;
+               try {
+                       Properties vars = new Properties();
+                       vars.putAll(parameters);
+                       String dirPath = uri.substring("file:".length());
+                       File homeDir = new File(dirPath);
+                       if (homeDir.exists() && !homeDir.isDirectory())
+                               throw new ArgeoJcrException("Repository home " + dirPath
+                                               + " is not a directory");
+                       if (!homeDir.exists())
+                               homeDir.mkdirs();
+                       configurationIn = fileRepositoryConfiguration.getInputStream();
+                       vars.put(RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE,
+                                       homeDir.getCanonicalPath());
+                       RepositoryConfig repositoryConfig = RepositoryConfig.create(
+                                       new InputSource(configurationIn), vars);
+
+                       // TransientRepository repository = new
+                       // TransientRepository(repositoryConfig);
+                       final RepositoryImpl repository = RepositoryImpl
+                                       .create(repositoryConfig);
+                       Session session = repository.login();
+                       // FIXME make it generic
+                       org.argeo.jcr.JcrUtils.addPrivilege(session, "/", "ROLE_ADMIN",
+                                       "jcr:all");
+                       org.argeo.jcr.JcrUtils.logoutQuietly(session);
+                       Runtime.getRuntime().addShutdownHook(
+                                       new Thread("Clean JCR repository " + uri) {
+                                               public void run() {
+                                                       repository.shutdown();
+                                                       log.info("Destroyed repository " + uri);
+                                               }
+                                       });
+                       log.info("Initialized file Jackrabbit repository from uri " + uri);
+                       return repository;
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot create repository " + uri, e);
+               } finally {
+                       IOUtils.closeQuietly(configurationIn);
+               }
+       }
+
+       protected String getAliasFromURI(String uri) {
+               try {
+                       URI uriObj = new URI(uri);
+                       String alias = uriObj.getPath();
+                       if (alias.charAt(0) == '/')
+                               alias = alias.substring(1);
+                       if (alias.charAt(alias.length() - 1) == '/')
+                               alias = alias.substring(0, alias.length() - 1);
+                       return alias;
+               } catch (URISyntaxException e) {
+                       throw new ArgeoJcrException("Cannot interpret URI " + uri, e);
+               }
+       }
+
+       /**
+        * Called after the repository has been initialised. Does nothing by
+        * default.
+        */
+       @SuppressWarnings("rawtypes")
+       protected void postInitialization(Repository repository, Map parameters) {
+
+       }
+
+       public void setFileRepositoryConfiguration(
+                       Resource fileRepositoryConfiguration) {
+               this.fileRepositoryConfiguration = fileRepositoryConfiguration;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java
new file mode 100644 (file)
index 0000000..a2eb983
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.jackrabbit.fs;
+
+import org.argeo.jcr.fs.JcrFileSystemProvider;
+
+public abstract class AbstractJackrabbitFsProvider extends JcrFileSystemProvider {
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java
new file mode 100644 (file)
index 0000000..764eed0
--- /dev/null
@@ -0,0 +1,139 @@
+package org.argeo.jackrabbit.fs;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+
+import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory;
+import org.argeo.jcr.ArgeoJcrException;
+import org.argeo.jcr.fs.JcrFileSystem;
+import org.argeo.jcr.fs.JcrFsException;
+
+public class DavexFsProvider extends AbstractJackrabbitFsProvider {
+       final static String JACKRABBIT_REPOSITORY_URI = "org.apache.jackrabbit.repository.uri";
+       final static String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "org.apache.jackrabbit.spi2davex.WorkspaceNameDefault";
+
+       private Map<String, JcrFileSystem> fileSystems = new HashMap<>();
+
+       @Override
+       public String getScheme() {
+               return "davex";
+       }
+
+       @Override
+       public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+               if (uri.getHost() == null)
+                       throw new ArgeoJcrException("An host should be provided");
+               try {
+                       URI repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null);
+                       String repoKey = repoUri.toString();
+                       if (fileSystems.containsKey(repoKey))
+                               throw new FileSystemAlreadyExistsException("CMS file system already exists for " + repoKey);
+                       RepositoryFactory repositoryFactory = new Jcr2davRepositoryFactory();
+                       return tryGetRepo(repositoryFactory, repoUri, "main");
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot open file system " + uri, e);
+               }
+       }
+
+       private JcrFileSystem tryGetRepo(RepositoryFactory repositoryFactory, URI repoUri, String workspace)
+                       throws IOException {
+               Map<String, String> params = new HashMap<String, String>();
+               params.put(JACKRABBIT_REPOSITORY_URI, repoUri.toString());
+               params.put(JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, "main");
+               Repository repository = null;
+               Session session = null;
+               try {
+                       repository = repositoryFactory.getRepository(params);
+                       if (repository != null)
+                               session = repository.login(workspace);
+               } catch (Exception e) {
+                       // silent
+               }
+
+               if (session == null) {
+                       if (repoUri.getPath() == null || repoUri.getPath().equals("/"))
+                               return null;
+                       String repoUriStr = repoUri.toString();
+                       if (repoUriStr.endsWith("/"))
+                               repoUriStr = repoUriStr.substring(0, repoUriStr.length() - 1);
+                       String nextRepoUriStr = repoUriStr.substring(0, repoUriStr.lastIndexOf('/'));
+                       String nextWorkspace = repoUriStr.substring(repoUriStr.lastIndexOf('/') + 1);
+                       URI nextUri;
+                       try {
+                               nextUri = new URI(nextRepoUriStr);
+                       } catch (URISyntaxException e) {
+                               throw new ArgeoJcrException("Badly formatted URI", e);
+                       }
+                       return tryGetRepo(repositoryFactory, nextUri, nextWorkspace);
+               } else {
+                       JcrFileSystem fileSystem = new JcrFileSystem(this, session);
+                       fileSystems.put(repoUri.toString() + "/" + workspace, fileSystem);
+                       return fileSystem;
+               }
+       }
+
+       @Override
+       public FileSystem getFileSystem(URI uri) {
+               return currentUserFileSystem(uri);
+       }
+
+       @Override
+       public Path getPath(URI uri) {
+               JcrFileSystem fileSystem = currentUserFileSystem(uri);
+               if (fileSystem == null)
+                       try {
+                               fileSystem = (JcrFileSystem) newFileSystem(uri, new HashMap<String, Object>());
+                       } catch (IOException e) {
+                               throw new JcrFsException("Could not autocreate file system", e);
+                       }
+               URI repoUri = null;
+               try {
+                       repoUri = new URI("http", uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath(), null, null);
+               } catch (URISyntaxException e) {
+                       // TODO Auto-generated catch block
+                       e.printStackTrace();
+               }
+               String uriStr = repoUri.toString();
+               String localPath = null;
+               for (String key : fileSystems.keySet()) {
+                       if (uriStr.startsWith(key)) {
+                               localPath = uriStr.toString().substring(key.length());
+                       }
+               }
+               return fileSystem.getPath(localPath);
+       }
+
+       private JcrFileSystem currentUserFileSystem(URI uri) {
+               for (String key : fileSystems.keySet()) {
+                       if (uri.toString().startsWith(key))
+                               return fileSystems.get(key);
+               }
+               return null;
+       }
+
+       public static void main(String args[]) {
+               try {
+                       DavexFsProvider fsProvider = new DavexFsProvider();
+                       Path path = fsProvider.getPath(new URI("davex://root:demo@localhost:7070/jcr/node/main/home/"));
+                       System.out.println(path);
+                       DirectoryStream<Path> ds = Files.newDirectoryStream(path);
+                       for (Path p : ds) {
+                               System.out.println("- " + p);
+                       }
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+}
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java
new file mode 100644 (file)
index 0000000..47cf33d
--- /dev/null
@@ -0,0 +1,64 @@
+package org.argeo.jackrabbit.fs;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.argeo.jcr.fs.JcrFileSystem;
+import org.argeo.jcr.fs.JcrFsException;
+
+public class JackrabbitMemoryFsProvider extends AbstractJackrabbitFsProvider {
+       private RepositoryImpl repository;
+       private JcrFileSystem fileSystem;
+
+       @Override
+       public String getScheme() {
+               return "jcr+memory";
+       }
+
+       @Override
+       public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+               try {
+                       Path tempDir = Files.createTempDirectory("fs-memory");
+                       URL confUrl = getClass().getResource("fs-memory.xml");
+                       RepositoryConfig repositoryConfig = RepositoryConfig.create(confUrl.toURI(), tempDir.toString());
+                       repository = RepositoryImpl.create(repositoryConfig);
+                       String username = System.getProperty("user.name");
+                       Session session = repository.login(new SimpleCredentials(username, username.toCharArray()));
+                       fileSystem = new JcrFileSystem(this, session);
+                       return fileSystem;
+               } catch (RepositoryException | URISyntaxException e) {
+                       throw new IOException("Cannot login to repository", e);
+               }
+       }
+
+       @Override
+       public FileSystem getFileSystem(URI uri) {
+               return fileSystem;
+       }
+
+       @Override
+       public Path getPath(URI uri) {
+               String path = uri.getPath();
+               if (fileSystem == null)
+                       try {
+                               newFileSystem(uri, new HashMap<String, Object>());
+                       } catch (IOException e) {
+                               throw new JcrFsException("Could not autocreate file system", e);
+                       }
+               return fileSystem.getPath(path);
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml
new file mode 100644 (file)
index 0000000..f2541fb
--- /dev/null
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem
+               class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="main" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem
+                       class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+               </PersistenceManager>
+               <SearchIndex
+                       class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="directoryManagerClass"
+                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+                       <param name="extractorPoolSize" value="0" />
+                       <FileSystem
+                               class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem
+                       class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex
+               class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="directoryManagerClass"
+                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+               <param name="extractorPoolSize" value="0" />
+               <FileSystem
+                       class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <LoginModule
+                       class="org.apache.jackrabbit.core.security.SimpleLoginModule" />
+               <!-- <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager" -->
+               <!-- workspaceName="security" /> -->
+               <!-- <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" 
+                       /> -->
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/repository-h2.xml b/org.argeo.jcr/src/org/argeo/jackrabbit/repository-h2.xml
new file mode 100644 (file)
index 0000000..0526762
--- /dev/null
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.h2.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="h2" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="default" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="default" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="default" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/repository-localfs.xml b/org.argeo.jcr/src/org/argeo/jackrabbit/repository-localfs.xml
new file mode 100644 (file)
index 0000000..3d24708
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+               <param name="path" value="${rep.home}/repository" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${wsp.home}" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+                       <param name="path" value="${rep.home}/version" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/repository-memory.xml b/org.argeo.jcr/src/org/argeo/jackrabbit/repository-memory.xml
new file mode 100644 (file)
index 0000000..ecee5bd
--- /dev/null
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="directoryManagerClass"
+                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+                       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="directoryManagerClass"
+                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml b/org.argeo.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml
new file mode 100644 (file)
index 0000000..07a0d04
--- /dev/null
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.postgresql.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="postgresql" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="postgresql" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.FileDataStore">
+               <param name="path" value="${rep.home}/datastore" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml b/org.argeo.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml
new file mode 100644 (file)
index 0000000..9677828
--- /dev/null
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "Jackrabbit 2.6" "http://jackrabbit.apache.org/dtd/repository-2.6.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.postgresql.Driver" />
+                       <param name="url" value="${dburl}" />
+                       <param name="user" value="${dbuser}" />
+                       <param name="password" value="${dbpassword}" />
+                       <param name="databaseType" value="postgresql" />
+                       <param name="maxPoolSize" value="${maxPoolSize}" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="postgresql" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="${defaultWorkspace}" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+                       <param name="extractorPoolSize" value="${extractorPoolSize}" />
+                       <param name="cacheSize" value="${searchCacheSize}" />
+                       <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+               </SearchIndex>
+               <WorkspaceSecurity>
+                       <AccessControlProvider
+                               class="org.argeo.security.jackrabbit.ArgeoAccessControlProvider" />
+               </WorkspaceSecurity>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="postgresql" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.PostgreSQLPersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+                       <param name="bundleCacheSize" value="${bundleCacheMB}" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/index" />
+               <param name="extractorPoolSize" value="${extractorPoolSize}" />
+               <param name="cacheSize" value="${searchCacheSize}" />
+               <param name="maxVolatileIndexSize" value="${maxVolatileIndexSize}" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager class="org.argeo.security.jackrabbit.ArgeoSecurityManager"
+                       workspaceName="security" />
+               <AccessManager class="org.argeo.security.jackrabbit.ArgeoAccessManager" />
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java b/org.argeo.jcr/src/org/argeo/jackrabbit/unit/AbstractJackrabbitTestCase.java
new file mode 100644 (file)
index 0000000..1523c83
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jackrabbit.unit;
+
+import java.net.URL;
+
+import javax.jcr.Repository;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.jackrabbit.core.RepositoryImpl;
+import org.apache.jackrabbit.core.config.RepositoryConfig;
+import org.argeo.jcr.unit.AbstractJcrTestCase;
+
+/** Factorizes configuration of an in memory transient repository */
+public abstract class AbstractJackrabbitTestCase extends AbstractJcrTestCase {
+       protected RepositoryImpl repositoryImpl;
+
+       // protected File getRepositoryFile() throws Exception {
+       // Resource res = new ClassPathResource(
+       // "org/argeo/jackrabbit/unit/repository-memory.xml");
+       // return res.getFile();
+       // }
+
+       public AbstractJackrabbitTestCase() {
+               URL url = AbstractJackrabbitTestCase.class.getResource("jaas.config");
+               assert url != null;
+               System.setProperty("java.security.auth.login.config", url.toString());
+       }
+
+       protected Repository createRepository() throws Exception {
+               // Repository repository = new TransientRepository(getRepositoryFile(),
+               // getHomeDir());
+               RepositoryConfig repositoryConfig = RepositoryConfig.create(
+                               AbstractJackrabbitTestCase.class
+                                               .getResourceAsStream(getRepositoryConfigResource()),
+                               getHomeDir().getAbsolutePath());
+               RepositoryImpl repositoryImpl = RepositoryImpl.create(repositoryConfig);
+               return repositoryImpl;
+       }
+
+       protected String getRepositoryConfigResource() {
+               return "repository-memory.xml";
+       }
+
+       @Override
+       protected void clearRepository(Repository repository) throws Exception {
+               RepositoryImpl repositoryImpl = (RepositoryImpl) repository;
+               if (repositoryImpl != null)
+                       repositoryImpl.shutdown();
+               FileUtils.deleteDirectory(getHomeDir());
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/unit/jaas.config b/org.argeo.jcr/src/org/argeo/jackrabbit/unit/jaas.config
new file mode 100644 (file)
index 0000000..0313f91
--- /dev/null
@@ -0,0 +1,7 @@
+TEST_JACKRABBIT_ADMIN {
+   org.argeo.cms.auth.DataAdminLoginModule requisite;
+};
+
+Jackrabbit {
+   org.argeo.security.jackrabbit.SystemJackrabbitLoginModule requisite;
+};
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/unit/repository-h2.xml b/org.argeo.jcr/src/org/argeo/jackrabbit/unit/repository-h2.xml
new file mode 100644 (file)
index 0000000..348dc28
--- /dev/null
@@ -0,0 +1,81 @@
+<?xml version="1.0"?>
+<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
+                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
+<Repository>
+       <!-- Shared datasource -->
+       <DataSources>
+               <DataSource name="dataSource">
+                       <param name="driver" value="org.h2.Driver" />
+                       <param name="url" value="jdbc:h2:mem:jackrabbit" />
+                       <param name="user" value="sa" />
+                       <param name="password" value="" />
+                       <param name="databaseType" value="h2" />
+                       <param name="maxPoolSize" value="10" />
+               </DataSource>
+       </DataSources>
+
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schema" value="default" />
+               <param name="schemaObjectPrefix" value="fs_" />
+       </FileSystem>
+       <DataStore class="org.apache.jackrabbit.core.data.db.DbDataStore">
+               <param name="dataSourceName" value="dataSource" />
+               <param name="schemaObjectPrefix" value="ds_" />
+       </DataStore>
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="dev" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="default" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_fs_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="${wsp.name}_pm_" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${wsp.home}/index" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.db.DbFileSystem">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schema" value="default" />
+                       <param name="schemaObjectPrefix" value="fs_ver_" />
+               </FileSystem>
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.pool.H2PersistenceManager">
+                       <param name="dataSourceName" value="dataSource" />
+                       <param name="schemaObjectPrefix" value="pm_ver_" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/repository/index" />
+               <param name="extractorPoolSize" value="2" />
+               <param name="supportHighlighting" value="true" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"
+                       workspaceName="security" />
+               <AccessManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleAccessManager" />
+               <LoginModule
+                       class="org.apache.jackrabbit.core.security.simple.SimpleLoginModule">
+                       <param name="anonymousId" value="anonymous" />
+                       <param name="adminId" value="admin" />
+               </LoginModule>
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/unit/repository-memory.xml b/org.argeo.jcr/src/org/argeo/jackrabbit/unit/repository-memory.xml
new file mode 100644 (file)
index 0000000..8395424
--- /dev/null
@@ -0,0 +1,72 @@
+<?xml version="1.0"?>
+<!--
+
+    Copyright (C) 2007-2012 Argeo GmbH
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE Repository PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 1.6//EN"
+                            "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
+<Repository>
+       <!-- File system and datastore -->
+       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+
+       <!-- Workspace templates -->
+       <Workspaces rootPath="${rep.home}/workspaces"
+               defaultWorkspace="main" configRootPath="/workspaces" />
+       <Workspace name="${wsp.name}">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+               <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+                       <param name="path" value="${rep.home}/repository/index" />
+                       <param name="directoryManagerClass"
+                               value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+                       <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               </SearchIndex>
+       </Workspace>
+
+       <!-- Versioning -->
+       <Versioning rootPath="${rep.home}/version">
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+               <PersistenceManager
+                       class="org.apache.jackrabbit.core.persistence.bundle.BundleFsPersistenceManager">
+                       <param name="blobFSBlockSize" value="1" />
+               </PersistenceManager>
+       </Versioning>
+
+       <!-- Indexing -->
+       <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+               <param name="path" value="${rep.home}/repository/index" />
+               <param name="directoryManagerClass"
+                       value="org.apache.jackrabbit.core.query.lucene.directory.RAMDirectoryManager" />
+               <FileSystem class="org.apache.jackrabbit.core.fs.mem.MemoryFileSystem" />
+       </SearchIndex>
+
+       <!-- Security -->
+       <Security appName="Jackrabbit">
+               <SecurityManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleSecurityManager"
+                       workspaceName="security" />
+               <AccessManager
+                       class="org.apache.jackrabbit.core.security.simple.SimpleAccessManager" />
+               <LoginModule
+                       class="org.apache.jackrabbit.core.security.simple.SimpleLoginModule">
+                       <param name="anonymousId" value="anonymous" />
+                       <param name="adminId" value="admin" />
+               </LoginModule>
+       </Security>
+</Repository>
\ No newline at end of file
diff --git a/org.argeo.jcr/src/org/argeo/jcr/ArgeoJcrException.java b/org.argeo.jcr/src/org/argeo/jcr/ArgeoJcrException.java
new file mode 100644 (file)
index 0000000..09a645b
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.jcr;
+
+/** Argeo JCR specific exceptions. */
+public class ArgeoJcrException extends RuntimeException {
+       private static final long serialVersionUID = -1941940005390084331L;
+
+       public ArgeoJcrException(String message, Throwable e) {
+               super(message, e);
+       }
+
+       public ArgeoJcrException(String message) {
+               super(message);
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/Bin.java b/org.argeo.jcr/src/org/argeo/jcr/Bin.java
new file mode 100644 (file)
index 0000000..0418810
--- /dev/null
@@ -0,0 +1,60 @@
+package org.argeo.jcr;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.jcr.Binary;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+
+/**
+ * A {@link Binary} wrapper implementing {@link AutoCloseable} for ease of use
+ * in try/catch blocks.
+ */
+public class Bin implements Binary, AutoCloseable {
+       private final Binary wrappedBinary;
+
+       public Bin(Property property) throws RepositoryException {
+               this(property.getBinary());
+       }
+
+       public Bin(Binary wrappedBinary) {
+               if (wrappedBinary == null)
+                       throw new IllegalArgumentException("Wrapped binary cannot be null");
+               this.wrappedBinary = wrappedBinary;
+       }
+
+       // private static Binary getBinary(Property property) throws IOException {
+       // try {
+       // return property.getBinary();
+       // } catch (RepositoryException e) {
+       // throw new IOException("Cannot get binary from property " + property, e);
+       // }
+       // }
+
+       @Override
+       public void close() {
+               dispose();
+       }
+
+       @Override
+       public InputStream getStream() throws RepositoryException {
+               return wrappedBinary.getStream();
+       }
+
+       @Override
+       public int read(byte[] b, long position) throws IOException, RepositoryException {
+               return wrappedBinary.read(b, position);
+       }
+
+       @Override
+       public long getSize() throws RepositoryException {
+               return wrappedBinary.getSize();
+       }
+
+       @Override
+       public void dispose() {
+               wrappedBinary.dispose();
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/CollectionNodeIterator.java b/org.argeo.jcr/src/org/argeo/jcr/CollectionNodeIterator.java
new file mode 100644 (file)
index 0000000..a65907a
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+
+/** Wraps a collection of nodes in order to read it as a {@link NodeIterator} */
+public class CollectionNodeIterator implements NodeIterator {
+       private final Long collectionSize;
+       private final Iterator<Node> iterator;
+       private Integer position = 0;
+
+       public CollectionNodeIterator(Collection<Node> nodes) {
+               super();
+               this.collectionSize = (long) nodes.size();
+               this.iterator = nodes.iterator();
+       }
+
+       public void skip(long skipNum) {
+               if (skipNum < 0)
+                       throw new IllegalArgumentException(
+                                       "Skip count has to be positive: " + skipNum);
+
+               for (long i = 0; i < skipNum; i++) {
+                       if (!hasNext())
+                               throw new NoSuchElementException("Last element past (position="
+                                               + getPosition() + ")");
+                       nextNode();
+               }
+       }
+
+       public long getSize() {
+               return collectionSize;
+       }
+
+       public long getPosition() {
+               return position;
+       }
+
+       public boolean hasNext() {
+               return iterator.hasNext();
+       }
+
+       public Object next() {
+               return nextNode();
+       }
+
+       public void remove() {
+               iterator.remove();
+       }
+
+       public Node nextNode() {
+               Node node = iterator.next();
+               position++;
+               return node;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/DefaultJcrListener.java b/org.argeo.jcr/src/org/argeo/jcr/DefaultJcrListener.java
new file mode 100644 (file)
index 0000000..5ef8edd
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+import javax.jcr.observation.ObservationManager;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/** To be overridden */
+public class DefaultJcrListener implements EventListener {
+       private final static Log log = LogFactory.getLog(DefaultJcrListener.class);
+       private Session session;
+       private String path = "/";
+       private Boolean deep = true;
+
+       public void start() {
+               try {
+                       addEventListener(session().getWorkspace().getObservationManager());
+                       if (log.isDebugEnabled())
+                               log.debug("Registered JCR event listener on " + path);
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot register event listener", e);
+               }
+       }
+
+       public void stop() {
+               try {
+                       session().getWorkspace().getObservationManager()
+                                       .removeEventListener(this);
+                       if (log.isDebugEnabled())
+                               log.debug("Unregistered JCR event listener on " + path);
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot unregister event listener", e);
+               }
+       }
+
+       /** Default is listen to all events */
+       protected Integer getEvents() {
+               return Event.NODE_ADDED | Event.NODE_REMOVED | Event.PROPERTY_ADDED
+                               | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED;
+       }
+
+       /** To be overidden */
+       public void onEvent(EventIterator events) {
+               while (events.hasNext()) {
+                       Event event = events.nextEvent();
+                       log.debug(event);
+               }
+       }
+
+       /** To be overidden */
+       protected void addEventListener(ObservationManager observationManager)
+                       throws RepositoryException {
+               observationManager.addEventListener(this, getEvents(), path, deep,
+                               null, null, false);
+       }
+
+       private Session session() {
+               return session;
+       }
+
+       public void setPath(String path) {
+               this.path = path;
+       }
+
+       public void setDeep(Boolean deep) {
+               this.deep = deep;
+       }
+
+       public void setSession(Session session) {
+               this.session = session;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrAuthorizations.java b/org.argeo.jcr/src/org/argeo/jcr/JcrAuthorizations.java
new file mode 100644 (file)
index 0000000..46748b9
--- /dev/null
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.security.AccessControlManager;
+import javax.jcr.security.Privilege;
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+/** Apply authorizations to a JCR repository. */
+public class JcrAuthorizations implements Runnable {
+       // private final static Log log =
+       // LogFactory.getLog(JcrAuthorizations.class);
+
+       private Repository repository;
+       private String workspace = null;
+
+       private String securityWorkspace = "security";
+
+       /**
+        * key := privilege1,privilege2/path/to/node<br/>
+        * value := group1,group2,user1
+        */
+       private Map<String, String> principalPrivileges = new HashMap<String, String>();
+
+       public void run() {
+               String currentWorkspace = workspace;
+               Session session = null;
+               try {
+                       if (workspace != null && workspace.equals("*")) {
+                               session = repository.login();
+                               String[] workspaces = session.getWorkspace().getAccessibleWorkspaceNames();
+                               JcrUtils.logoutQuietly(session);
+                               for (String wksp : workspaces) {
+                                       currentWorkspace = wksp;
+                                       if (currentWorkspace.equals(securityWorkspace))
+                                               continue;
+                                       session = repository.login(currentWorkspace);
+                                       initAuthorizations(session);
+                                       JcrUtils.logoutQuietly(session);
+                               }
+                       } else {
+                               session = repository.login(workspace);
+                               initAuthorizations(session);
+                       }
+               } catch (Exception e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new ArgeoJcrException(
+                                       "Cannot set authorizations " + principalPrivileges + " on workspace " + currentWorkspace, e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       protected void processWorkspace(String workspace) {
+               Session session = null;
+               try {
+                       session = repository.login(workspace);
+                       initAuthorizations(session);
+               } catch (Exception e) {
+                       JcrUtils.discardQuietly(session);
+                       throw new ArgeoJcrException(
+                                       "Cannot set authorizations " + principalPrivileges + " on repository " + repository, e);
+               } finally {
+                       JcrUtils.logoutQuietly(session);
+               }
+       }
+
+       /** @deprecated call {@link #run()} instead. */
+       @Deprecated
+       public void init() {
+               run();
+       }
+
+       protected void initAuthorizations(Session session) throws RepositoryException {
+               AccessControlManager acm = session.getAccessControlManager();
+
+               for (String privileges : principalPrivileges.keySet()) {
+                       String path = null;
+                       int slashIndex = privileges.indexOf('/');
+                       if (slashIndex == 0) {
+                               throw new ArgeoJcrException("Privilege " + privileges + " badly formatted it starts with /");
+                       } else if (slashIndex > 0) {
+                               path = privileges.substring(slashIndex);
+                               privileges = privileges.substring(0, slashIndex);
+                       }
+
+                       if (path == null)
+                               path = "/";
+
+                       List<Privilege> privs = new ArrayList<Privilege>();
+                       for (String priv : privileges.split(",")) {
+                               privs.add(acm.privilegeFromName(priv));
+                       }
+
+                       String principalNames = principalPrivileges.get(privileges);
+                       try {
+                               new LdapName(principalNames);
+                               // TODO differentiate groups and users ?
+                               Principal principal = getOrCreatePrincipal(session, principalNames);
+                               JcrUtils.addPrivileges(session, path, principal, privs);
+                       } catch (InvalidNameException e) {
+                               for (String principalName : principalNames.split(",")) {
+                                       Principal principal = getOrCreatePrincipal(session, principalName);
+                                       JcrUtils.addPrivileges(session, path, principal, privs);
+                                       // if (log.isDebugEnabled()) {
+                                       // StringBuffer privBuf = new StringBuffer();
+                                       // for (Privilege priv : privs)
+                                       // privBuf.append(priv.getName());
+                                       // log.debug("Added privileges " + privBuf + " to "
+                                       // + principal.getName() + " on " + path + " in '"
+                                       // + session.getWorkspace().getName() + "'");
+                                       // }
+                               }
+                       }
+               }
+
+               // if (log.isDebugEnabled())
+               // log.debug("JCR authorizations applied on '"
+               // + session.getWorkspace().getName() + "'");
+       }
+
+       /**
+        * Returns a {@link SimplePrincipal}, does not check whether it exists since
+        * such capabilities is not provided by the standard JCR API. Can be
+        * overridden to provide smarter handling
+        */
+       protected Principal getOrCreatePrincipal(Session session, String principalName) throws RepositoryException {
+               return new SimplePrincipal(principalName);
+       }
+
+       // public static void addPrivileges(Session session, Principal principal,
+       // String path, List<Privilege> privs) throws RepositoryException {
+       // AccessControlManager acm = session.getAccessControlManager();
+       // // search for an access control list
+       // AccessControlList acl = null;
+       // AccessControlPolicyIterator policyIterator = acm
+       // .getApplicablePolicies(path);
+       // if (policyIterator.hasNext()) {
+       // while (policyIterator.hasNext()) {
+       // AccessControlPolicy acp = policyIterator
+       // .nextAccessControlPolicy();
+       // if (acp instanceof AccessControlList)
+       // acl = ((AccessControlList) acp);
+       // }
+       // } else {
+       // AccessControlPolicy[] existingPolicies = acm.getPolicies(path);
+       // for (AccessControlPolicy acp : existingPolicies) {
+       // if (acp instanceof AccessControlList)
+       // acl = ((AccessControlList) acp);
+       // }
+       // }
+       //
+       // if (acl != null) {
+       // acl.addAccessControlEntry(principal,
+       // privs.toArray(new Privilege[privs.size()]));
+       // acm.setPolicy(path, acl);
+       // session.save();
+       // if (log.isDebugEnabled()) {
+       // StringBuffer buf = new StringBuffer("");
+       // for (int i = 0; i < privs.size(); i++) {
+       // if (i != 0)
+       // buf.append(',');
+       // buf.append(privs.get(i).getName());
+       // }
+       // log.debug("Added privilege(s) '" + buf + "' to '"
+       // + principal.getName() + "' on " + path
+       // + " from workspace '"
+       // + session.getWorkspace().getName() + "'");
+       // }
+       // } else {
+       // throw new ArgeoJcrException("Don't know how to apply privileges "
+       // + privs + " to " + principal + " on " + path
+       // + " from workspace '" + session.getWorkspace().getName()
+       // + "'");
+       // }
+       // }
+
+       @Deprecated
+       public void setGroupPrivileges(Map<String, String> groupPrivileges) {
+               this.principalPrivileges = groupPrivileges;
+       }
+
+       public void setPrincipalPrivileges(Map<String, String> principalPrivileges) {
+               this.principalPrivileges = principalPrivileges;
+       }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setWorkspace(String workspace) {
+               this.workspace = workspace;
+       }
+
+       public void setSecurityWorkspace(String securityWorkspace) {
+               this.securityWorkspace = securityWorkspace;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrCallback.java b/org.argeo.jcr/src/org/argeo/jcr/JcrCallback.java
new file mode 100644 (file)
index 0000000..0c4706f
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import javax.jcr.Session;
+
+/** An arbitrary execution on a JCR session, optionally returning a result. */
+public interface JcrCallback {
+       public Object execute(Session session);
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrMonitor.java b/org.argeo.jcr/src/org/argeo/jcr/JcrMonitor.java
new file mode 100644 (file)
index 0000000..71cf961
--- /dev/null
@@ -0,0 +1,87 @@
+package org.argeo.jcr;
+
+
+/**
+ * Simple monitor abstraction. Inspired by Eclipse IProgressMOnitor, but without
+ * dependency to it.
+ */
+public interface JcrMonitor {
+       /**
+        * Constant indicating an unknown amount of work.
+        */
+       public final static int UNKNOWN = -1;
+
+       /**
+        * Notifies that the main task is beginning. This must only be called once
+        * on a given progress monitor instance.
+        * 
+        * @param name
+        *            the name (or description) of the main task
+        * @param totalWork
+        *            the total number of work units into which the main task is
+        *            been subdivided. If the value is <code>UNKNOWN</code> the
+        *            implementation is free to indicate progress in a way which
+        *            doesn't require the total number of work units in advance.
+        */
+       public void beginTask(String name, int totalWork);
+
+       /**
+        * Notifies that the work is done; that is, either the main task is
+        * completed or the user canceled it. This method may be called more than
+        * once (implementations should be prepared to handle this case).
+        */
+       public void done();
+
+       /**
+        * Returns whether cancelation of current operation has been requested.
+        * Long-running operations should poll to see if cancelation has been
+        * requested.
+        * 
+        * @return <code>true</code> if cancellation has been requested, and
+        *         <code>false</code> otherwise
+        * @see #setCanceled(boolean)
+        */
+       public boolean isCanceled();
+
+       /**
+        * Sets the cancel state to the given value.
+        * 
+        * @param value
+        *            <code>true</code> indicates that cancelation has been
+        *            requested (but not necessarily acknowledged);
+        *            <code>false</code> clears this flag
+        * @see #isCanceled()
+        */
+       public void setCanceled(boolean value);
+
+       /**
+        * Sets the task name to the given value. This method is used to restore the
+        * task label after a nested operation was executed. Normally there is no
+        * need for clients to call this method.
+        * 
+        * @param name
+        *            the name (or description) of the main task
+        * @see #beginTask(java.lang.String, int)
+        */
+       public void setTaskName(String name);
+
+       /**
+        * Notifies that a subtask of the main task is beginning. Subtasks are
+        * optional; the main task might not have subtasks.
+        * 
+        * @param name
+        *            the name (or description) of the subtask
+        */
+       public void subTask(String name);
+
+       /**
+        * Notifies that a given number of work unit of the main task has been
+        * completed. Note that this amount represents an installment, as opposed to
+        * a cumulative amount of work done to date.
+        * 
+        * @param work
+        *            a non-negative number of work units just completed
+        */
+       public void worked(int work);
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java b/org.argeo.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java
new file mode 100644 (file)
index 0000000..1e08c18
--- /dev/null
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Binary;
+import javax.jcr.Credentials;
+import javax.jcr.LoginException;
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+
+/**
+ * Wrapper around a JCR repository which allows to simplify configuration and
+ * intercept some actions. It exposes itself as a {@link Repository}.
+ */
+public abstract class JcrRepositoryWrapper implements Repository {
+       // private final static Log log = LogFactory
+       // .getLog(JcrRepositoryWrapper.class);
+
+       // wrapped repository
+       private Repository repository;
+
+       private Map<String, String> additionalDescriptors = new HashMap<>();
+
+       private Boolean autocreateWorkspaces = false;
+
+       public JcrRepositoryWrapper(Repository repository) {
+               setRepository(repository);
+       }
+
+       /**
+        * Empty constructor
+        */
+       public JcrRepositoryWrapper() {
+       }
+
+       // /** Initializes */
+       // public void init() {
+       // }
+       //
+       // /** Shutdown the repository */
+       // public void destroy() throws Exception {
+       // }
+
+       protected void putDescriptor(String key, String value) {
+               if (Arrays.asList(getRepository().getDescriptorKeys()).contains(key))
+                       throw new IllegalArgumentException("Descriptor key " + key + " is already defined in wrapped repository");
+               if (value == null)
+                       additionalDescriptors.remove(key);
+               else
+                       additionalDescriptors.put(key, value);
+       }
+
+       /*
+        * DELEGATED JCR REPOSITORY METHODS
+        */
+
+       public String getDescriptor(String key) {
+               if (additionalDescriptors.containsKey(key))
+                       return additionalDescriptors.get(key);
+               return getRepository().getDescriptor(key);
+       }
+
+       public String[] getDescriptorKeys() {
+               if (additionalDescriptors.size() == 0)
+                       return getRepository().getDescriptorKeys();
+               List<String> keys = Arrays.asList(getRepository().getDescriptorKeys());
+               keys.addAll(additionalDescriptors.keySet());
+               return keys.toArray(new String[keys.size()]);
+       }
+
+       /** Central login method */
+       public Session login(Credentials credentials, String workspaceName)
+                       throws LoginException, NoSuchWorkspaceException, RepositoryException {
+               Session session;
+               try {
+                       session = getRepository().login(credentials, workspaceName);
+               } catch (NoSuchWorkspaceException e) {
+                       if (autocreateWorkspaces && workspaceName != null)
+                               session = createWorkspaceAndLogsIn(credentials, workspaceName);
+                       else
+                               throw e;
+               }
+               processNewSession(session);
+               return session;
+       }
+
+       public Session login() throws LoginException, RepositoryException {
+               return login(null, null);
+       }
+
+       public Session login(Credentials credentials) throws LoginException, RepositoryException {
+               return login(credentials, null);
+       }
+
+       public Session login(String workspaceName) throws LoginException, NoSuchWorkspaceException, RepositoryException {
+               return login(null, workspaceName);
+       }
+
+       /** Called after a session has been created, does nothing by default. */
+       protected void processNewSession(Session session) {
+       }
+
+       /** Wraps access to the repository, making sure it is available. */
+       protected synchronized Repository getRepository() {
+               // if (repository == null) {
+               // throw new ArgeoJcrException("No repository initialized."
+               // + " Was the init() method called?"
+               // + " The destroy() method should also"
+               // + " be called on shutdown.");
+               // }
+               return repository;
+       }
+
+       /**
+        * Logs in to the default workspace, creates the required workspace, logs
+        * out, logs in to the required workspace.
+        */
+       protected Session createWorkspaceAndLogsIn(Credentials credentials, String workspaceName)
+                       throws RepositoryException {
+               if (workspaceName == null)
+                       throw new ArgeoJcrException("No workspace specified.");
+               Session session = getRepository().login(credentials);
+               session.getWorkspace().createWorkspace(workspaceName);
+               session.logout();
+               return getRepository().login(credentials, workspaceName);
+       }
+
+       public boolean isStandardDescriptor(String key) {
+               return getRepository().isStandardDescriptor(key);
+       }
+
+       public boolean isSingleValueDescriptor(String key) {
+               if (additionalDescriptors.containsKey(key))
+                       return true;
+               return getRepository().isSingleValueDescriptor(key);
+       }
+
+       public Value getDescriptorValue(String key) {
+               if (additionalDescriptors.containsKey(key))
+                       return new StrValue(additionalDescriptors.get(key));
+               return getRepository().getDescriptorValue(key);
+       }
+
+       public Value[] getDescriptorValues(String key) {
+               return getRepository().getDescriptorValues(key);
+       }
+
+       public synchronized void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setAutocreateWorkspaces(Boolean autocreateWorkspaces) {
+               this.autocreateWorkspaces = autocreateWorkspaces;
+       }
+
+       protected static class StrValue implements Value {
+               private final String str;
+
+               public StrValue(String str) {
+                       this.str = str;
+               }
+
+               @Override
+               public String getString() throws ValueFormatException, IllegalStateException, RepositoryException {
+                       return str;
+               }
+
+               @Override
+               public InputStream getStream() throws RepositoryException {
+                       throw new UnsupportedOperationException();
+               }
+
+               @Override
+               public Binary getBinary() throws RepositoryException {
+                       throw new UnsupportedOperationException();
+               }
+
+               @Override
+               public long getLong() throws ValueFormatException, RepositoryException {
+                       try {
+                               return Long.parseLong(str);
+                       } catch (NumberFormatException e) {
+                               throw new ValueFormatException("Cannot convert", e);
+                       }
+               }
+
+               @Override
+               public double getDouble() throws ValueFormatException, RepositoryException {
+                       try {
+                               return Double.parseDouble(str);
+                       } catch (NumberFormatException e) {
+                               throw new ValueFormatException("Cannot convert", e);
+                       }
+               }
+
+               @Override
+               public BigDecimal getDecimal() throws ValueFormatException, RepositoryException {
+                       try {
+                               return new BigDecimal(str);
+                       } catch (NumberFormatException e) {
+                               throw new ValueFormatException("Cannot convert", e);
+                       }
+               }
+
+               @Override
+               public Calendar getDate() throws ValueFormatException, RepositoryException {
+                       throw new UnsupportedOperationException();
+               }
+
+               @Override
+               public boolean getBoolean() throws ValueFormatException, RepositoryException {
+                       try {
+                               return Boolean.parseBoolean(str);
+                       } catch (NumberFormatException e) {
+                               throw new ValueFormatException("Cannot convert", e);
+                       }
+               }
+
+               @Override
+               public int getType() {
+                       return PropertyType.STRING;
+               }
+
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrResourceAdapter.java b/org.argeo.jcr/src/org/argeo/jcr/JcrResourceAdapter.java
new file mode 100644 (file)
index 0000000..be7bf49
--- /dev/null
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.version.Version;
+import javax.jcr.version.VersionHistory;
+import javax.jcr.version.VersionIterator;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Bridge Spring resources and JCR folder / files semantics (nt:folder /
+ * nt:file), supporting versioning as well.
+ */
+@Deprecated
+public class JcrResourceAdapter {
+       private final static Log log = LogFactory.getLog(JcrResourceAdapter.class);
+
+       private Session session;
+
+       private Boolean versioning = true;
+       private String defaultEncoding = "UTF-8";
+
+       // private String restoreBase = "/.restore";
+
+       public JcrResourceAdapter() {
+       }
+
+       public JcrResourceAdapter(Session session) {
+               this.session = session;
+       }
+
+       public void mkdirs(String path) {
+               JcrUtils.mkdirs(session(), path, NodeType.NT_FOLDER,
+                               NodeType.NT_FOLDER, versioning);
+       }
+
+       public void create(String path, InputStream in, String mimeType) {
+               try {
+                       if (session().itemExists(path)) {
+                               throw new ArgeoJcrException("Node " + path + " already exists.");
+                       }
+
+                       int index = path.lastIndexOf('/');
+                       String parentPath = path.substring(0, index);
+                       if (parentPath.equals(""))
+                               parentPath = "/";
+                       String fileName = path.substring(index + 1);
+                       if (!session().itemExists(parentPath))
+                               throw new ArgeoJcrException("Parent folder of node " + path
+                                               + " does not exist: " + parentPath);
+
+                       Node folderNode = (Node) session().getItem(parentPath);
+                       Node fileNode = folderNode.addNode(fileName, "nt:file");
+
+                       Node contentNode = fileNode.addNode(Property.JCR_CONTENT,
+                                       "nt:resource");
+                       if (mimeType != null)
+                               contentNode.setProperty(Property.JCR_MIMETYPE, mimeType);
+                       contentNode.setProperty(Property.JCR_ENCODING, defaultEncoding);
+                       Binary binary = session().getValueFactory().createBinary(in);
+                       contentNode.setProperty(Property.JCR_DATA, binary);
+                       JcrUtils.closeQuietly(binary);
+                       Calendar lastModified = Calendar.getInstance();
+                       // lastModified.setTimeInMillis(file.lastModified());
+                       contentNode.setProperty(Property.JCR_LAST_MODIFIED, lastModified);
+                       // resNode.addMixin("mix:referenceable");
+
+                       if (versioning)
+                               fileNode.addMixin("mix:versionable");
+
+                       session().save();
+
+                       if (versioning)
+                               session().getWorkspace().getVersionManager()
+                                               .checkin(fileNode.getPath());
+
+                       if (log.isDebugEnabled())
+                               log.debug("Created " + path);
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot create node for " + path, e);
+               }
+
+       }
+
+       public void update(String path, InputStream in) {
+               try {
+
+                       if (!session().itemExists(path)) {
+                               String type = null;
+                               // FIXME: using javax.activation leads to conflict between Java
+                               // 1.5 and 1.6 (since javax.activation was included in Java 1.6)
+                               // String type = new MimetypesFileTypeMap()
+                               // .getContentType(FilenameUtils.getName(path));
+                               create(path, in, type);
+                               return;
+                       }
+
+                       Node fileNode = (Node) session().getItem(path);
+                       Node contentNode = fileNode.getNode(Property.JCR_CONTENT);
+                       if (versioning)
+                               session().getWorkspace().getVersionManager()
+                                               .checkout(fileNode.getPath());
+                       Binary binary = session().getValueFactory().createBinary(in);
+                       contentNode.setProperty(Property.JCR_DATA, binary);
+                       JcrUtils.closeQuietly(binary);
+                       Calendar lastModified = Calendar.getInstance();
+                       // lastModified.setTimeInMillis(file.lastModified());
+                       contentNode.setProperty(Property.JCR_LAST_MODIFIED, lastModified);
+
+                       session().save();
+                       if (versioning)
+                               session().getWorkspace().getVersionManager()
+                                               .checkin(fileNode.getPath());
+
+                       if (log.isDebugEnabled())
+                               log.debug("Updated " + path);
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot update node " + path, e);
+               }
+       }
+
+       public List<Calendar> listVersions(String path) {
+               if (!versioning)
+                       throw new ArgeoJcrException("Versioning is not activated");
+
+               try {
+                       List<Calendar> versions = new ArrayList<Calendar>();
+                       Node fileNode = (Node) session().getItem(path);
+                       VersionHistory history = session().getWorkspace()
+                                       .getVersionManager().getVersionHistory(fileNode.getPath());
+                       for (VersionIterator it = history.getAllVersions(); it.hasNext();) {
+                               Version version = (Version) it.next();
+                               versions.add(version.getCreated());
+                               if (log.isTraceEnabled()) {
+                                       log.debug(version);
+                                       // debug(version);
+                               }
+                       }
+                       return versions;
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot list version of node " + path, e);
+               }
+       }
+
+       public InputStream retrieve(String path) {
+               try {
+                       Node node = (Node) session().getItem(
+                                       path + "/" + Property.JCR_CONTENT);
+                       Property property = node.getProperty(Property.JCR_DATA);
+                       return property.getBinary().getStream();
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot retrieve " + path, e);
+               }
+       }
+
+       public synchronized InputStream retrieve(String path, Integer revision) {
+               if (!versioning)
+                       throw new ArgeoJcrException("Versioning is not activated");
+
+               try {
+                       Node fileNode = (Node) session().getItem(path);
+                       VersionHistory history = session().getWorkspace()
+                                       .getVersionManager().getVersionHistory(fileNode.getPath());
+                       int count = 0;
+                       Version version = null;
+                       for (VersionIterator it = history.getAllVersions(); it.hasNext();) {
+                               version = (Version) it.next();
+                               if (count == revision + 1) {
+                                       InputStream in = fromVersion(version);
+                                       if (log.isDebugEnabled())
+                                               log.debug("Retrieved " + path + " at revision "
+                                                               + revision);
+                                       return in;
+                               }
+                               count++;
+                       }
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot retrieve version " + revision
+                                       + " of " + path, e);
+               }
+
+               throw new ArgeoJcrException("Version " + revision
+                               + " does not exist for node " + path);
+       }
+
+       protected InputStream fromVersion(Version version)
+                       throws RepositoryException {
+               Node frozenNode = version.getNode("jcr:frozenNode");
+               InputStream in = frozenNode.getNode(Property.JCR_CONTENT)
+                               .getProperty(Property.JCR_DATA).getBinary().getStream();
+               return in;
+       }
+
+       protected Session session() {
+               return session;
+       }
+
+       public void setVersioning(Boolean versioning) {
+               this.versioning = versioning;
+       }
+
+       public void setDefaultEncoding(String defaultEncoding) {
+               this.defaultEncoding = defaultEncoding;
+       }
+
+       protected String fill(Integer number) {
+               int size = 4;
+               String str = number.toString();
+               for (int i = str.length(); i < size; i++) {
+                       str = "0" + str;
+               }
+               return str;
+       }
+
+       public void setSession(Session session) {
+               this.session = session;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java b/org.argeo.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java
new file mode 100644 (file)
index 0000000..a777639
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+/** URL stream handler able to deal with nt:file node and properties. NOT FINISHED */
+public class JcrUrlStreamHandler extends URLStreamHandler {
+       private final Session session;
+
+       public JcrUrlStreamHandler(Session session) {
+               this.session = session;
+       }
+
+       @Override
+       protected URLConnection openConnection(final URL u) throws IOException {
+               // TODO Auto-generated method stub
+               return new URLConnection(u) {
+
+                       @Override
+                       public void connect() throws IOException {
+                               String itemPath = u.getPath();
+                               try {
+                                       if (!session.itemExists(itemPath))
+                                               throw new IOException("No item under " + itemPath);
+
+                                       Item item = session.getItem(u.getPath());
+                                       if (item.isNode()) {
+                                               // this should be a nt:file node
+                                               Node node = (Node) item;
+                                               if (!node.getPrimaryNodeType().isNodeType(
+                                                               NodeType.NT_FILE))
+                                                       throw new IOException("Node " + node + " is not a "
+                                                                       + NodeType.NT_FILE);
+
+                                       } else {
+                                               Property property = (Property) item;
+                                               if(property.getType()==PropertyType.BINARY){
+                                                       //Binary binary = property.getBinary();
+                                                       
+                                               }
+                                       }
+                               } catch (RepositoryException e) {
+                                       IOException ioe = new IOException(
+                                                       "Unexpected JCR exception");
+                                       ioe.initCause(e);
+                                       throw ioe;
+                               }
+                       }
+
+                       @Override
+                       public InputStream getInputStream() throws IOException {
+                               // TODO Auto-generated method stub
+                               return super.getInputStream();
+                       }
+
+               };
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java b/org.argeo.jcr/src/org/argeo/jcr/JcrUtils.java
new file mode 100644 (file)
index 0000000..158b52a
--- /dev/null
@@ -0,0 +1,1551 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.Principal;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.jcr.Binary;
+import javax.jcr.NamespaceRegistry;
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.Workspace;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.observation.EventListener;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryResult;
+import javax.jcr.security.AccessControlEntry;
+import javax.jcr.security.AccessControlList;
+import javax.jcr.security.AccessControlManager;
+import javax.jcr.security.AccessControlPolicy;
+import javax.jcr.security.AccessControlPolicyIterator;
+import javax.jcr.security.Privilege;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/** Utility methods to simplify common JCR operations. */
+public class JcrUtils {
+
+       final private static Log log = LogFactory.getLog(JcrUtils.class);
+
+       /**
+        * Not complete yet. See
+        * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.2.2%20Local
+        * %20Names
+        */
+       public final static char[] INVALID_NAME_CHARACTERS = { '/', ':', '[', ']', '|', '*', /* invalid for XML: */ '<',
+                       '>', '&' };
+
+       /** Prevents instantiation */
+       private JcrUtils() {
+       }
+
+       /**
+        * Queries one single node.
+        * 
+        * @return one single node or null if none was found
+        * @throws ArgeoJcrException
+        *             if more than one node was found
+        */
+       public static Node querySingleNode(Query query) {
+               NodeIterator nodeIterator;
+               try {
+                       QueryResult queryResult = query.execute();
+                       nodeIterator = queryResult.getNodes();
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot execute query " + query, e);
+               }
+               Node node;
+               if (nodeIterator.hasNext())
+                       node = nodeIterator.nextNode();
+               else
+                       return null;
+
+               if (nodeIterator.hasNext())
+                       throw new ArgeoJcrException("Query returned more than one node.");
+               return node;
+       }
+
+       /** Retrieves the node name from the provided path */
+       public static String nodeNameFromPath(String path) {
+               if (path.equals("/"))
+                       return "";
+               if (path.charAt(0) != '/')
+                       throw new ArgeoJcrException("Path " + path + " must start with a '/'");
+               String pathT = path;
+               if (pathT.charAt(pathT.length() - 1) == '/')
+                       pathT = pathT.substring(0, pathT.length() - 2);
+
+               int index = pathT.lastIndexOf('/');
+               return pathT.substring(index + 1);
+       }
+
+       /** Retrieves the parent path of the provided path */
+       public static String parentPath(String path) {
+               if (path.equals("/"))
+                       throw new ArgeoJcrException("Root path '/' has no parent path");
+               if (path.charAt(0) != '/')
+                       throw new ArgeoJcrException("Path " + path + " must start with a '/'");
+               String pathT = path;
+               if (pathT.charAt(pathT.length() - 1) == '/')
+                       pathT = pathT.substring(0, pathT.length() - 2);
+
+               int index = pathT.lastIndexOf('/');
+               return pathT.substring(0, index);
+       }
+
+       /** The provided data as a path ('/' at the end, not the beginning) */
+       public static String dateAsPath(Calendar cal) {
+               return dateAsPath(cal, false);
+       }
+
+       /**
+        * Creates a deep path based on a URL:
+        * http://subdomain.example.com/to/content?args becomes
+        * com/example/subdomain/to/content
+        */
+       public static String urlAsPath(String url) {
+               try {
+                       URL u = new URL(url);
+                       StringBuffer path = new StringBuffer(url.length());
+                       // invert host
+                       path.append(hostAsPath(u.getHost()));
+                       // we don't put port since it may not always be there and may change
+                       path.append(u.getPath());
+                       return path.toString();
+               } catch (MalformedURLException e) {
+                       throw new ArgeoJcrException("Cannot generate URL path for " + url, e);
+               }
+       }
+
+       /** Set the {@link NodeType#NT_ADDRESS} properties based on this URL. */
+       public static void urlToAddressProperties(Node node, String url) {
+               try {
+                       URL u = new URL(url);
+                       node.setProperty(Property.JCR_PROTOCOL, u.getProtocol());
+                       node.setProperty(Property.JCR_HOST, u.getHost());
+                       node.setProperty(Property.JCR_PORT, Integer.toString(u.getPort()));
+                       node.setProperty(Property.JCR_PATH, normalizePath(u.getPath()));
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot set URL " + url + " as nt:address properties", e);
+               }
+       }
+
+       /** Build URL based on the {@link NodeType#NT_ADDRESS} properties. */
+       public static String urlFromAddressProperties(Node node) {
+               try {
+                       URL u = new URL(node.getProperty(Property.JCR_PROTOCOL).getString(),
+                                       node.getProperty(Property.JCR_HOST).getString(),
+                                       (int) node.getProperty(Property.JCR_PORT).getLong(),
+                                       node.getProperty(Property.JCR_PATH).getString());
+                       return u.toString();
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot get URL from nt:address properties of " + node, e);
+               }
+       }
+
+       /*
+        * PATH UTILITIES
+        */
+
+       /**
+        * Make sure that: starts with '/', do not end with '/', do not have '//'
+        */
+       public static String normalizePath(String path) {
+               List<String> tokens = tokenize(path);
+               StringBuffer buf = new StringBuffer(path.length());
+               for (String token : tokens) {
+                       buf.append('/');
+                       buf.append(token);
+               }
+               return buf.toString();
+       }
+
+       /**
+        * Creates a path from a FQDN, inverting the order of the component:
+        * www.argeo.org becomes org.argeo.www
+        */
+       public static String hostAsPath(String host) {
+               StringBuffer path = new StringBuffer(host.length());
+               String[] hostTokens = host.split("\\.");
+               for (int i = hostTokens.length - 1; i >= 0; i--) {
+                       path.append(hostTokens[i]);
+                       if (i != 0)
+                               path.append('/');
+               }
+               return path.toString();
+       }
+
+       /**
+        * Creates a path from a UUID (e.g. 6ebda899-217d-4bf1-abe4-2839085c8f3c becomes
+        * 6ebda899-217d/4bf1/abe4/2839085c8f3c/). '/' at the end, not the beginning
+        */
+       public static String uuidAsPath(String uuid) {
+               StringBuffer path = new StringBuffer(uuid.length());
+               String[] tokens = uuid.split("-");
+               for (int i = 0; i < tokens.length; i++) {
+                       path.append(tokens[i]);
+                       if (i != 0)
+                               path.append('/');
+               }
+               return path.toString();
+       }
+
+       /**
+        * The provided data as a path ('/' at the end, not the beginning)
+        * 
+        * @param cal
+        *            the date
+        * @param addHour
+        *            whether to add hour as well
+        */
+       public static String dateAsPath(Calendar cal, Boolean addHour) {
+               StringBuffer buf = new StringBuffer(14);
+               buf.append('Y');
+               buf.append(cal.get(Calendar.YEAR));
+               buf.append('/');
+
+               int month = cal.get(Calendar.MONTH) + 1;
+               buf.append('M');
+               if (month < 10)
+                       buf.append(0);
+               buf.append(month);
+               buf.append('/');
+
+               int day = cal.get(Calendar.DAY_OF_MONTH);
+               buf.append('D');
+               if (day < 10)
+                       buf.append(0);
+               buf.append(day);
+               buf.append('/');
+
+               if (addHour) {
+                       int hour = cal.get(Calendar.HOUR_OF_DAY);
+                       buf.append('H');
+                       if (hour < 10)
+                               buf.append(0);
+                       buf.append(hour);
+                       buf.append('/');
+               }
+               return buf.toString();
+
+       }
+
+       /** Converts in one call a string into a gregorian calendar. */
+       public static Calendar parseCalendar(DateFormat dateFormat, String value) {
+               try {
+                       Date date = dateFormat.parse(value);
+                       Calendar calendar = new GregorianCalendar();
+                       calendar.setTime(date);
+                       return calendar;
+               } catch (ParseException e) {
+                       throw new ArgeoJcrException("Cannot parse " + value + " with date format " + dateFormat, e);
+               }
+
+       }
+
+       /** The last element of a path. */
+       public static String lastPathElement(String path) {
+               if (path.charAt(path.length() - 1) == '/')
+                       throw new ArgeoJcrException("Path " + path + " cannot end with '/'");
+               int index = path.lastIndexOf('/');
+               if (index < 0)
+                       return path;
+               return path.substring(index + 1);
+       }
+
+       /**
+        * Call {@link Node#getName()} without exceptions (useful in super
+        * constructors).
+        */
+       public static String getNameQuietly(Node node) {
+               try {
+                       return node.getName();
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot get name from " + node, e);
+               }
+       }
+
+       /**
+        * Call {@link Node#getProperty(String)} without exceptions (useful in super
+        * constructors).
+        */
+       public static String getStringPropertyQuietly(Node node, String propertyName) {
+               try {
+                       return node.getProperty(propertyName).getString();
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot get name from " + node, e);
+               }
+       }
+
+       /**
+        * Routine that get the child with this name, adding id it does not already
+        * exist
+        */
+       public static Node getOrAdd(Node parent, String childName, String childPrimaryNodeType) throws RepositoryException {
+               return parent.hasNode(childName) ? parent.getNode(childName) : parent.addNode(childName, childPrimaryNodeType);
+       }
+
+       /**
+        * Routine that get the child with this name, adding id it does not already
+        * exist
+        */
+       public static Node getOrAdd(Node parent, String childName) throws RepositoryException {
+               return parent.hasNode(childName) ? parent.getNode(childName) : parent.addNode(childName);
+       }
+
+       /** Convert a {@link NodeIterator} to a list of {@link Node} */
+       public static List<Node> nodeIteratorToList(NodeIterator nodeIterator) {
+               List<Node> nodes = new ArrayList<Node>();
+               while (nodeIterator.hasNext()) {
+                       nodes.add(nodeIterator.nextNode());
+               }
+               return nodes;
+       }
+
+       /*
+        * PROPERTIES
+        */
+
+       /**
+        * Concisely get the string value of a property or null if this node doesn't
+        * have this property
+        */
+       public static String get(Node node, String propertyName) {
+               try {
+                       if (!node.hasProperty(propertyName))
+                               return null;
+                       return node.getProperty(propertyName).getString();
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot get property " + propertyName + " of " + node, e);
+               }
+       }
+
+       /** Concisely get the boolean value of a property */
+       public static Boolean check(Node node, String propertyName) {
+               try {
+                       return node.getProperty(propertyName).getBoolean();
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot get property " + propertyName + " of " + node, e);
+               }
+       }
+
+       /** Concisely get the bytes array value of a property */
+       public static byte[] getBytes(Node node, String propertyName) {
+               try {
+                       return getBinaryAsBytes(node.getProperty(propertyName));
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot get property " + propertyName + " of " + node, e);
+               }
+       }
+
+       /*
+        * MKDIRS
+        */
+
+       /**
+        * Create sub nodes relative to a parent node
+        */
+       public static Node mkdirs(Node parentNode, String relativePath) {
+               return mkdirs(parentNode, relativePath, null, null);
+       }
+
+       /**
+        * Create sub nodes relative to a parent node
+        * 
+        * @param nodeType
+        *            the type of the leaf node
+        */
+       public static Node mkdirs(Node parentNode, String relativePath, String nodeType) {
+               return mkdirs(parentNode, relativePath, nodeType, null);
+       }
+
+       /**
+        * Create sub nodes relative to a parent node
+        * 
+        * @param nodeType
+        *            the type of the leaf node
+        */
+       public static Node mkdirs(Node parentNode, String relativePath, String nodeType, String intermediaryNodeType) {
+               List<String> tokens = tokenize(relativePath);
+               Node currParent = parentNode;
+               try {
+                       for (int i = 0; i < tokens.size(); i++) {
+                               String name = tokens.get(i);
+                               if (currParent.hasNode(name)) {
+                                       currParent = currParent.getNode(name);
+                               } else {
+                                       if (i != (tokens.size() - 1)) {// intermediary
+                                               currParent = currParent.addNode(name, intermediaryNodeType);
+                                       } else {// leaf
+                                               currParent = currParent.addNode(name, nodeType);
+                                       }
+                               }
+                       }
+                       return currParent;
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot mkdirs relative path " + relativePath + " from " + parentNode, e);
+               }
+       }
+
+       /**
+        * Synchronized and save is performed, to avoid race conditions in initializers
+        * leading to duplicate nodes.
+        */
+       public synchronized static Node mkdirsSafe(Session session, String path, String type) {
+               try {
+                       if (session.hasPendingChanges())
+                               throw new ArgeoJcrException("Session has pending changes, save them first.");
+                       Node node = mkdirs(session, path, type);
+                       session.save();
+                       return node;
+               } catch (RepositoryException e) {
+                       discardQuietly(session);
+                       throw new ArgeoJcrException("Cannot safely make directories", e);
+               }
+       }
+
+       public synchronized static Node mkdirsSafe(Session session, String path) {
+               return mkdirsSafe(session, path, null);
+       }
+
+       /** Creates the nodes making path, if they don't exist. */
+       public static Node mkdirs(Session session, String path) {
+               return mkdirs(session, path, null, null, false);
+       }
+
+       /**
+        * @param type
+        *            the type of the leaf node
+        */
+       public static Node mkdirs(Session session, String path, String type) {
+               return mkdirs(session, path, type, null, false);
+       }
+
+       /**
+        * Creates the nodes making path, if they don't exist. This is up to the caller
+        * to save the session. Use with caution since it can create duplicate nodes if
+        * used concurrently. Requires read access to the root node of the workspace.
+        */
+       public static Node mkdirs(Session session, String path, String type, String intermediaryNodeType,
+                       Boolean versioning) {
+               try {
+                       if (path.equals("/"))
+                               return session.getRootNode();
+
+                       if (session.itemExists(path)) {
+                               Node node = session.getNode(path);
+                               // check type
+                               if (type != null && !node.isNodeType(type) && !node.getPath().equals("/"))
+                                       throw new ArgeoJcrException("Node " + node + " exists but is of type "
+                                                       + node.getPrimaryNodeType().getName() + " not of type " + type);
+                               // TODO: check versioning
+                               return node;
+                       }
+
+                       // StringBuffer current = new StringBuffer("/");
+                       // Node currentNode = session.getRootNode();
+
+                       Node currentNode = findClosestExistingParent(session, path);
+                       String closestExistingParentPath = currentNode.getPath();
+                       StringBuffer current = new StringBuffer(closestExistingParentPath);
+                       if (!closestExistingParentPath.endsWith("/"))
+                               current.append('/');
+                       Iterator<String> it = tokenize(path.substring(closestExistingParentPath.length())).iterator();
+                       while (it.hasNext()) {
+                               String part = it.next();
+                               current.append(part).append('/');
+                               if (!session.itemExists(current.toString())) {
+                                       if (!it.hasNext() && type != null)
+                                               currentNode = currentNode.addNode(part, type);
+                                       else if (it.hasNext() && intermediaryNodeType != null)
+                                               currentNode = currentNode.addNode(part, intermediaryNodeType);
+                                       else
+                                               currentNode = currentNode.addNode(part);
+                                       if (versioning)
+                                               currentNode.addMixin(NodeType.MIX_VERSIONABLE);
+                                       if (log.isTraceEnabled())
+                                               log.debug("Added folder " + part + " as " + current);
+                               } else {
+                                       currentNode = (Node) session.getItem(current.toString());
+                               }
+                       }
+                       return currentNode;
+               } catch (RepositoryException e) {
+                       discardQuietly(session);
+                       throw new ArgeoJcrException("Cannot mkdirs " + path, e);
+               } finally {
+               }
+       }
+
+       private static Node findClosestExistingParent(Session session, String path) throws RepositoryException {
+               int idx = path.lastIndexOf('/');
+               if (idx == 0)
+                       return session.getRootNode();
+               String parentPath = path.substring(0, idx);
+               if (session.itemExists(parentPath))
+                       return session.getNode(parentPath);
+               else
+                       return findClosestExistingParent(session, parentPath);
+       }
+
+       /** Convert a path to the list of its tokens */
+       public static List<String> tokenize(String path) {
+               List<String> tokens = new ArrayList<String>();
+               boolean optimized = false;
+               if (!optimized) {
+                       String[] rawTokens = path.split("/");
+                       for (String token : rawTokens) {
+                               if (!token.equals(""))
+                                       tokens.add(token);
+                       }
+               } else {
+                       StringBuffer curr = new StringBuffer();
+                       char[] arr = path.toCharArray();
+                       chars: for (int i = 0; i < arr.length; i++) {
+                               char c = arr[i];
+                               if (c == '/') {
+                                       if (i == 0 || (i == arr.length - 1))
+                                               continue chars;
+                                       if (curr.length() > 0) {
+                                               tokens.add(curr.toString());
+                                               curr = new StringBuffer();
+                                       }
+                               } else
+                                       curr.append(c);
+                       }
+                       if (curr.length() > 0) {
+                               tokens.add(curr.toString());
+                               curr = new StringBuffer();
+                       }
+               }
+               return Collections.unmodifiableList(tokens);
+       }
+
+       // /**
+       // * use {@link #mkdirs(Session, String, String, String, Boolean)} instead.
+       // *
+       // * @deprecated
+       // */
+       // @Deprecated
+       // public static Node mkdirs(Session session, String path, String type,
+       // Boolean versioning) {
+       // return mkdirs(session, path, type, type, false);
+       // }
+
+       /**
+        * Safe and repository implementation independent registration of a namespace.
+        */
+       public static void registerNamespaceSafely(Session session, String prefix, String uri) {
+               try {
+                       registerNamespaceSafely(session.getWorkspace().getNamespaceRegistry(), prefix, uri);
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot find namespace registry", e);
+               }
+       }
+
+       /**
+        * Safe and repository implementation independent registration of a namespace.
+        */
+       public static void registerNamespaceSafely(NamespaceRegistry nr, String prefix, String uri) {
+               try {
+                       String[] prefixes = nr.getPrefixes();
+                       for (String pref : prefixes)
+                               if (pref.equals(prefix)) {
+                                       String registeredUri = nr.getURI(pref);
+                                       if (!registeredUri.equals(uri))
+                                               throw new ArgeoJcrException("Prefix " + pref + " already registered for URI " + registeredUri
+                                                               + " which is different from provided URI " + uri);
+                                       else
+                                               return;// skip
+                               }
+                       nr.registerNamespace(prefix, uri);
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot register namespace " + uri + " under prefix " + prefix, e);
+               }
+       }
+
+       /** Recursively outputs the contents of the given node. */
+       public static void debug(Node node) {
+               debug(node, log);
+       }
+
+       /** Recursively outputs the contents of the given node. */
+       public static void debug(Node node, Log log) {
+               try {
+                       // First output the node path
+                       log.debug(node.getPath());
+                       // Skip the virtual (and large!) jcr:system subtree
+                       if (node.getName().equals("jcr:system")) {
+                               return;
+                       }
+
+                       // Then the children nodes (recursive)
+                       NodeIterator it = node.getNodes();
+                       while (it.hasNext()) {
+                               Node childNode = it.nextNode();
+                               debug(childNode, log);
+                       }
+
+                       // Then output the properties
+                       PropertyIterator properties = node.getProperties();
+                       // log.debug("Property are : ");
+
+                       properties: while (properties.hasNext()) {
+                               Property property = properties.nextProperty();
+                               if (property.getType() == PropertyType.BINARY)
+                                       continue properties;// skip
+                               if (property.getDefinition().isMultiple()) {
+                                       // A multi-valued property, print all values
+                                       Value[] values = property.getValues();
+                                       for (int i = 0; i < values.length; i++) {
+                                               log.debug(property.getPath() + "=" + values[i].getString());
+                                       }
+                               } else {
+                                       // A single-valued property
+                                       log.debug(property.getPath() + "=" + property.getString());
+                               }
+                       }
+               } catch (Exception e) {
+                       log.error("Could not debug " + node, e);
+               }
+
+       }
+
+       /** Logs the effective access control policies */
+       public static void logEffectiveAccessPolicies(Node node) {
+               try {
+                       logEffectiveAccessPolicies(node.getSession(), node.getPath());
+               } catch (RepositoryException e) {
+                       log.error("Cannot log effective access policies of " + node, e);
+               }
+       }
+
+       /** Logs the effective access control policies */
+       public static void logEffectiveAccessPolicies(Session session, String path) {
+               if (!log.isDebugEnabled())
+                       return;
+
+               try {
+                       AccessControlPolicy[] effectivePolicies = session.getAccessControlManager().getEffectivePolicies(path);
+                       if (effectivePolicies.length > 0) {
+                               for (AccessControlPolicy policy : effectivePolicies) {
+                                       if (policy instanceof AccessControlList) {
+                                               AccessControlList acl = (AccessControlList) policy;
+                                               log.debug("Access control list for " + path + "\n" + accessControlListSummary(acl));
+                                       }
+                               }
+                       } else {
+                               log.debug("No effective access control policy for " + path);
+                       }
+               } catch (RepositoryException e) {
+                       log.error("Cannot log effective access policies of " + path, e);
+               }
+       }
+
+       /** Returns a human-readable summary of this access control list. */
+       public static String accessControlListSummary(AccessControlList acl) {
+               StringBuffer buf = new StringBuffer("");
+               try {
+                       for (AccessControlEntry ace : acl.getAccessControlEntries()) {
+                               buf.append('\t').append(ace.getPrincipal().getName()).append('\n');
+                               for (Privilege priv : ace.getPrivileges())
+                                       buf.append("\t\t").append(priv.getName()).append('\n');
+                       }
+                       return buf.toString();
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot write summary of " + acl, e);
+               }
+       }
+
+       /**
+        * Copies recursively the content of a node to another one. Do NOT copy the
+        * property values of {@link NodeType#MIX_CREATED} and
+        * {@link NodeType#MIX_LAST_MODIFIED}, but update the
+        * {@link Property#JCR_LAST_MODIFIED} and {@link Property#JCR_LAST_MODIFIED_BY}
+        * properties if the target node has the {@link NodeType#MIX_LAST_MODIFIED}
+        * mixin.
+        */
+       public static void copy(Node fromNode, Node toNode) {
+               try {
+                       if (toNode.getDefinition().isProtected())
+                               return;
+
+                       // process properties
+                       PropertyIterator pit = fromNode.getProperties();
+                       properties: while (pit.hasNext()) {
+                               Property fromProperty = pit.nextProperty();
+                               String propertyName = fromProperty.getName();
+                               if (toNode.hasProperty(propertyName) && toNode.getProperty(propertyName).getDefinition().isProtected())
+                                       continue properties;
+
+                               if (fromProperty.getDefinition().isProtected())
+                                       continue properties;
+
+                               if (propertyName.equals("jcr:created") || propertyName.equals("jcr:createdBy")
+                                               || propertyName.equals("jcr:lastModified") || propertyName.equals("jcr:lastModifiedBy"))
+                                       continue properties;
+
+                               if (fromProperty.isMultiple()) {
+                                       toNode.setProperty(propertyName, fromProperty.getValues());
+                               } else {
+                                       toNode.setProperty(propertyName, fromProperty.getValue());
+                               }
+                       }
+
+                       // update jcr:lastModified and jcr:lastModifiedBy in toNode in case
+                       // they existed, before adding the mixins
+                       updateLastModified(toNode);
+
+                       // add mixins
+                       for (NodeType mixinType : fromNode.getMixinNodeTypes()) {
+                               toNode.addMixin(mixinType.getName());
+                       }
+
+                       // process children nodes
+                       NodeIterator nit = fromNode.getNodes();
+                       while (nit.hasNext()) {
+                               Node fromChild = nit.nextNode();
+                               Integer index = fromChild.getIndex();
+                               String nodeRelPath = fromChild.getName() + "[" + index + "]";
+                               Node toChild;
+                               if (toNode.hasNode(nodeRelPath))
+                                       toChild = toNode.getNode(nodeRelPath);
+                               else
+                                       toChild = toNode.addNode(fromChild.getName(), fromChild.getPrimaryNodeType().getName());
+                               copy(fromChild, toChild);
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot copy " + fromNode + " to " + toNode, e);
+               }
+       }
+
+       /**
+        * Check whether all first-level properties (except jcr:* properties) are equal.
+        * Skip jcr:* properties
+        */
+       public static Boolean allPropertiesEquals(Node reference, Node observed, Boolean onlyCommonProperties) {
+               try {
+                       PropertyIterator pit = reference.getProperties();
+                       props: while (pit.hasNext()) {
+                               Property propReference = pit.nextProperty();
+                               String propName = propReference.getName();
+                               if (propName.startsWith("jcr:"))
+                                       continue props;
+
+                               if (!observed.hasProperty(propName))
+                                       if (onlyCommonProperties)
+                                               continue props;
+                                       else
+                                               return false;
+                               // TODO: deal with multiple property values?
+                               if (!observed.getProperty(propName).getValue().equals(propReference.getValue()))
+                                       return false;
+                       }
+                       return true;
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot check all properties equals of " + reference + " and " + observed, e);
+               }
+       }
+
+       public static Map<String, PropertyDiff> diffProperties(Node reference, Node observed) {
+               Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
+               diffPropertiesLevel(diffs, null, reference, observed);
+               return diffs;
+       }
+
+       /**
+        * Compare the properties of two nodes. Recursivity to child nodes is not yet
+        * supported. Skip jcr:* properties.
+        */
+       static void diffPropertiesLevel(Map<String, PropertyDiff> diffs, String baseRelPath, Node reference,
+                       Node observed) {
+               try {
+                       // check removed and modified
+                       PropertyIterator pit = reference.getProperties();
+                       props: while (pit.hasNext()) {
+                               Property p = pit.nextProperty();
+                               String name = p.getName();
+                               if (name.startsWith("jcr:"))
+                                       continue props;
+
+                               if (!observed.hasProperty(name)) {
+                                       String relPath = propertyRelPath(baseRelPath, name);
+                                       PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED, relPath, p.getValue(), null);
+                                       diffs.put(relPath, pDiff);
+                               } else {
+                                       if (p.isMultiple()) {
+                                               // FIXME implement multiple
+                                       } else {
+                                               Value referenceValue = p.getValue();
+                                               Value newValue = observed.getProperty(name).getValue();
+                                               if (!referenceValue.equals(newValue)) {
+                                                       String relPath = propertyRelPath(baseRelPath, name);
+                                                       PropertyDiff pDiff = new PropertyDiff(PropertyDiff.MODIFIED, relPath, referenceValue,
+                                                                       newValue);
+                                                       diffs.put(relPath, pDiff);
+                                               }
+                                       }
+                               }
+                       }
+                       // check added
+                       pit = observed.getProperties();
+                       props: while (pit.hasNext()) {
+                               Property p = pit.nextProperty();
+                               String name = p.getName();
+                               if (name.startsWith("jcr:"))
+                                       continue props;
+                               if (!reference.hasProperty(name)) {
+                                       if (p.isMultiple()) {
+                                               // FIXME implement multiple
+                                       } else {
+                                               String relPath = propertyRelPath(baseRelPath, name);
+                                               PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED, relPath, null, p.getValue());
+                                               diffs.put(relPath, pDiff);
+                                       }
+                               }
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot diff " + reference + " and " + observed, e);
+               }
+       }
+
+       /**
+        * Compare only a restricted list of properties of two nodes. No recursivity.
+        * 
+        */
+       public static Map<String, PropertyDiff> diffProperties(Node reference, Node observed, List<String> properties) {
+               Map<String, PropertyDiff> diffs = new TreeMap<String, PropertyDiff>();
+               try {
+                       Iterator<String> pit = properties.iterator();
+
+                       props: while (pit.hasNext()) {
+                               String name = pit.next();
+                               if (!reference.hasProperty(name)) {
+                                       if (!observed.hasProperty(name))
+                                               continue props;
+                                       Value val = observed.getProperty(name).getValue();
+                                       try {
+                                               // empty String but not null
+                                               if ("".equals(val.getString()))
+                                                       continue props;
+                                       } catch (Exception e) {
+                                               // not parseable as String, silent
+                                       }
+                                       PropertyDiff pDiff = new PropertyDiff(PropertyDiff.ADDED, name, null, val);
+                                       diffs.put(name, pDiff);
+                               } else if (!observed.hasProperty(name)) {
+                                       PropertyDiff pDiff = new PropertyDiff(PropertyDiff.REMOVED, name,
+                                                       reference.getProperty(name).getValue(), null);
+                                       diffs.put(name, pDiff);
+                               } else {
+                                       Value referenceValue = reference.getProperty(name).getValue();
+                                       Value newValue = observed.getProperty(name).getValue();
+                                       if (!referenceValue.equals(newValue)) {
+                                               PropertyDiff pDiff = new PropertyDiff(PropertyDiff.MODIFIED, name, referenceValue, newValue);
+                                               diffs.put(name, pDiff);
+                                       }
+                               }
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot diff " + reference + " and " + observed, e);
+               }
+               return diffs;
+       }
+
+       /** Builds a property relPath to be used in the diff. */
+       private static String propertyRelPath(String baseRelPath, String propertyName) {
+               if (baseRelPath == null)
+                       return propertyName;
+               else
+                       return baseRelPath + '/' + propertyName;
+       }
+
+       /**
+        * Normalizes a name so that it can be stored in contexts not supporting names
+        * with ':' (typically databases). Replaces ':' by '_'.
+        */
+       public static String normalize(String name) {
+               return name.replace(':', '_');
+       }
+
+       /**
+        * Replaces characters which are invalid in a JCR name by '_'. Currently not
+        * exhaustive.
+        * 
+        * @see JcrUtils#INVALID_NAME_CHARACTERS
+        */
+       public static String replaceInvalidChars(String name) {
+               return replaceInvalidChars(name, '_');
+       }
+
+       /**
+        * Replaces characters which are invalid in a JCR name. Currently not
+        * exhaustive.
+        * 
+        * @see JcrUtils#INVALID_NAME_CHARACTERS
+        */
+       public static String replaceInvalidChars(String name, char replacement) {
+               boolean modified = false;
+               char[] arr = name.toCharArray();
+               for (int i = 0; i < arr.length; i++) {
+                       char c = arr[i];
+                       invalid: for (char invalid : INVALID_NAME_CHARACTERS) {
+                               if (c == invalid) {
+                                       arr[i] = replacement;
+                                       modified = true;
+                                       break invalid;
+                               }
+                       }
+               }
+               if (modified)
+                       return new String(arr);
+               else
+                       // do not create new object if unnecessary
+                       return name;
+       }
+
+       // /**
+       // * Removes forbidden characters from a path, replacing them with '_'
+       // *
+       // * @deprecated use {@link #replaceInvalidChars(String)} instead
+       // */
+       // public static String removeForbiddenCharacters(String str) {
+       // return str.replace('[', '_').replace(']', '_').replace('/', '_').replace('*',
+       // '_');
+       //
+       // }
+
+       /** Cleanly disposes a {@link Binary} even if it is null. */
+       public static void closeQuietly(Binary binary) {
+               if (binary == null)
+                       return;
+               binary.dispose();
+       }
+
+       /** Retrieve a {@link Binary} as a byte array */
+       public static byte[] getBinaryAsBytes(Property property) {
+               // ByteArrayOutputStream out = new ByteArrayOutputStream();
+               // InputStream in = null;
+               // Binary binary = null;
+               try (ByteArrayOutputStream out = new ByteArrayOutputStream();
+                               Bin binary = new Bin(property);
+                               InputStream in = binary.getStream()) {
+                       // binary = property.getBinary();
+                       // in = binary.getStream();
+                       IOUtils.copy(in, out);
+                       return out.toByteArray();
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot read binary " + property + " as bytes", e);
+               } finally {
+                       // IOUtils.closeQuietly(out);
+                       // IOUtils.closeQuietly(in);
+                       // closeQuietly(binary);
+               }
+       }
+
+       /** Writes a {@link Binary} from a byte array */
+       public static void setBinaryAsBytes(Node node, String property, byte[] bytes) {
+               // InputStream in = null;
+               Binary binary = null;
+               try (InputStream in = new ByteArrayInputStream(bytes)) {
+                       // in = new ByteArrayInputStream(bytes);
+                       binary = node.getSession().getValueFactory().createBinary(in);
+                       node.setProperty(property, binary);
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot read binary " + property + " as bytes", e);
+               } finally {
+                       // IOUtils.closeQuietly(in);
+                       closeQuietly(binary);
+               }
+       }
+
+       /**
+        * Creates depth from a string (typically a username) by adding levels based on
+        * its first characters: "aBcD",2 becomes a/aB
+        */
+       public static String firstCharsToPath(String str, Integer nbrOfChars) {
+               if (str.length() < nbrOfChars)
+                       throw new ArgeoJcrException("String " + str + " length must be greater or equal than " + nbrOfChars);
+               StringBuffer path = new StringBuffer("");
+               StringBuffer curr = new StringBuffer("");
+               for (int i = 0; i < nbrOfChars; i++) {
+                       curr.append(str.charAt(i));
+                       path.append(curr);
+                       if (i < nbrOfChars - 1)
+                               path.append('/');
+               }
+               return path.toString();
+       }
+
+       /**
+        * Discards the current changes in the session attached to this node. To be used
+        * typically in a catch block.
+        * 
+        * @see #discardQuietly(Session)
+        */
+       public static void discardUnderlyingSessionQuietly(Node node) {
+               try {
+                       discardQuietly(node.getSession());
+               } catch (RepositoryException e) {
+                       log.warn("Cannot quietly discard session of node " + node + ": " + e.getMessage());
+               }
+       }
+
+       /**
+        * Discards the current changes in a session by calling
+        * {@link Session#refresh(boolean)} with <code>false</code>, only logging
+        * potential errors when doing so. To be used typically in a catch block.
+        */
+       public static void discardQuietly(Session session) {
+               try {
+                       if (session != null)
+                               session.refresh(false);
+               } catch (RepositoryException e) {
+                       log.warn("Cannot quietly discard session " + session + ": " + e.getMessage());
+               }
+       }
+
+       /**
+        * Login to a workspace with implicit credentials, creates the workspace with
+        * these credentials if it does not already exist.
+        */
+       public static Session loginOrCreateWorkspace(Repository repository, String workspaceName)
+                       throws RepositoryException {
+               Session workspaceSession = null;
+               Session defaultSession = null;
+               try {
+                       try {
+                               workspaceSession = repository.login(workspaceName);
+                       } catch (NoSuchWorkspaceException e) {
+                               // try to create workspace
+                               defaultSession = repository.login();
+                               defaultSession.getWorkspace().createWorkspace(workspaceName);
+                               workspaceSession = repository.login(workspaceName);
+                       }
+                       return workspaceSession;
+               } finally {
+                       logoutQuietly(defaultSession);
+               }
+       }
+
+       /** Logs out the session, not throwing any exception, even if it is null. */
+       public static void logoutQuietly(Session session) {
+               try {
+                       if (session != null)
+                               if (session.isLive())
+                                       session.logout();
+               } catch (Exception e) {
+                       // silent
+               }
+       }
+
+       /**
+        * Convenient method to add a listener. uuids passed as null, deep=true,
+        * local=true, only one node type
+        */
+       public static void addListener(Session session, EventListener listener, int eventTypes, String basePath,
+                       String nodeType) {
+               try {
+                       session.getWorkspace().getObservationManager().addEventListener(listener, eventTypes, basePath, true, null,
+                                       nodeType == null ? null : new String[] { nodeType }, true);
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot add JCR listener " + listener + " to session " + session, e);
+               }
+       }
+
+       /** Removes a listener without throwing exception */
+       public static void removeListenerQuietly(Session session, EventListener listener) {
+               if (session == null || !session.isLive())
+                       return;
+               try {
+                       session.getWorkspace().getObservationManager().removeEventListener(listener);
+               } catch (RepositoryException e) {
+                       // silent
+               }
+       }
+
+       /**
+        * Quietly unregisters an {@link EventListener} from the udnerlying workspace of
+        * this node.
+        */
+       public static void unregisterQuietly(Node node, EventListener eventListener) {
+               try {
+                       unregisterQuietly(node.getSession().getWorkspace(), eventListener);
+               } catch (RepositoryException e) {
+                       // silent
+                       if (log.isTraceEnabled())
+                               log.trace("Could not unregister event listener " + eventListener);
+               }
+       }
+
+       /** Quietly unregisters an {@link EventListener} from this workspace */
+       public static void unregisterQuietly(Workspace workspace, EventListener eventListener) {
+               if (eventListener == null)
+                       return;
+               try {
+                       workspace.getObservationManager().removeEventListener(eventListener);
+               } catch (RepositoryException e) {
+                       // silent
+                       if (log.isTraceEnabled())
+                               log.trace("Could not unregister event listener " + eventListener);
+               }
+       }
+
+       /**
+        * If this node is has the {@link NodeType#MIX_LAST_MODIFIED} mixin, it updates
+        * the {@link Property#JCR_LAST_MODIFIED} property with the current time and the
+        * {@link Property#JCR_LAST_MODIFIED_BY} property with the underlying session
+        * user id. In Jackrabbit 2.x,
+        * <a href="https://issues.apache.org/jira/browse/JCR-2233">these properties are
+        * not automatically updated</a>, hence the need for manual update. The session
+        * is not saved.
+        */
+       public static void updateLastModified(Node node) {
+               try {
+                       if (!node.isNodeType(NodeType.MIX_LAST_MODIFIED))
+                               node.addMixin(NodeType.MIX_LAST_MODIFIED);
+                       node.setProperty(Property.JCR_LAST_MODIFIED, new GregorianCalendar());
+                       node.setProperty(Property.JCR_LAST_MODIFIED_BY, node.getSession().getUserID());
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot update last modified on " + node, e);
+               }
+       }
+
+       /**
+        * Update lastModified recursively until this parent.
+        * 
+        * @param node
+        *            the node
+        * @param untilPath
+        *            the base path, null is equivalent to "/"
+        */
+       public static void updateLastModifiedAndParents(Node node, String untilPath) {
+               try {
+                       if (untilPath != null && !node.getPath().startsWith(untilPath))
+                               throw new ArgeoJcrException(node + " is not under " + untilPath);
+                       updateLastModified(node);
+                       if (untilPath == null) {
+                               if (!node.getPath().equals("/"))
+                                       updateLastModifiedAndParents(node.getParent(), untilPath);
+                       } else {
+                               if (!node.getPath().equals(untilPath))
+                                       updateLastModifiedAndParents(node.getParent(), untilPath);
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot update lastModified from " + node + " until " + untilPath, e);
+               }
+       }
+
+       /**
+        * Returns a String representing the short version (see
+        * <a href="http://jackrabbit.apache.org/node-type-notation.html"> Node type
+        * Notation </a> attributes grammar) of the main business attributes of this
+        * property definition
+        * 
+        * @param prop
+        */
+       public static String getPropertyDefinitionAsString(Property prop) {
+               StringBuffer sbuf = new StringBuffer();
+               try {
+                       if (prop.getDefinition().isAutoCreated())
+                               sbuf.append("a");
+                       if (prop.getDefinition().isMandatory())
+                               sbuf.append("m");
+                       if (prop.getDefinition().isProtected())
+                               sbuf.append("p");
+                       if (prop.getDefinition().isMultiple())
+                               sbuf.append("*");
+               } catch (RepositoryException re) {
+                       throw new ArgeoJcrException("unexpected error while getting property definition as String", re);
+               }
+               return sbuf.toString();
+       }
+
+       /**
+        * Estimate the sub tree size from current node. Computation is based on the Jcr
+        * {@link Property#getLength()} method. Note : it is not the exact size used on
+        * the disk by the current part of the JCR Tree.
+        */
+
+       public static long getNodeApproxSize(Node node) {
+               long curNodeSize = 0;
+               try {
+                       PropertyIterator pi = node.getProperties();
+                       while (pi.hasNext()) {
+                               Property prop = pi.nextProperty();
+                               if (prop.isMultiple()) {
+                                       int nb = prop.getLengths().length;
+                                       for (int i = 0; i < nb; i++) {
+                                               curNodeSize += (prop.getLengths()[i] > 0 ? prop.getLengths()[i] : 0);
+                                       }
+                               } else
+                                       curNodeSize += (prop.getLength() > 0 ? prop.getLength() : 0);
+                       }
+
+                       NodeIterator ni = node.getNodes();
+                       while (ni.hasNext())
+                               curNodeSize += getNodeApproxSize(ni.nextNode());
+                       return curNodeSize;
+               } catch (RepositoryException re) {
+                       throw new ArgeoJcrException("Unexpected error while recursively determining node size.", re);
+               }
+       }
+
+       /*
+        * SECURITY
+        */
+
+       /**
+        * Convenience method for adding a single privilege to a principal (user or
+        * role), typically jcr:all
+        */
+       public synchronized static void addPrivilege(Session session, String path, String principal, String privilege)
+                       throws RepositoryException {
+               List<Privilege> privileges = new ArrayList<Privilege>();
+               privileges.add(session.getAccessControlManager().privilegeFromName(privilege));
+               addPrivileges(session, path, new SimplePrincipal(principal), privileges);
+       }
+
+       /**
+        * Add privileges on a path to a {@link Principal}. The path must already exist.
+        * Session is saved. Synchronized to prevent concurrent modifications of the
+        * same node.
+        */
+       public synchronized static Boolean addPrivileges(Session session, String path, Principal principal,
+                       List<Privilege> privs) throws RepositoryException {
+               // make sure the session is in line with the persisted state
+               session.refresh(false);
+               AccessControlManager acm = session.getAccessControlManager();
+               AccessControlList acl = getAccessControlList(acm, path);
+
+               accessControlEntries: for (AccessControlEntry ace : acl.getAccessControlEntries()) {
+                       Principal currentPrincipal = ace.getPrincipal();
+                       if (currentPrincipal.getName().equals(principal.getName())) {
+                               Privilege[] currentPrivileges = ace.getPrivileges();
+                               if (currentPrivileges.length != privs.size())
+                                       break accessControlEntries;
+                               for (int i = 0; i < currentPrivileges.length; i++) {
+                                       Privilege currP = currentPrivileges[i];
+                                       Privilege p = privs.get(i);
+                                       if (!currP.getName().equals(p.getName())) {
+                                               break accessControlEntries;
+                                       }
+                               }
+                               return false;
+                       }
+               }
+
+               Privilege[] privileges = privs.toArray(new Privilege[privs.size()]);
+               acl.addAccessControlEntry(principal, privileges);
+               acm.setPolicy(path, acl);
+               if (log.isDebugEnabled()) {
+                       StringBuffer privBuf = new StringBuffer();
+                       for (Privilege priv : privs)
+                               privBuf.append(priv.getName());
+                       log.debug("Added privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '"
+                                       + session.getWorkspace().getName() + "'");
+               }
+               session.refresh(true);
+               session.save();
+               return true;
+       }
+
+       /** Gets access control list for this path, throws exception if not found */
+       public synchronized static AccessControlList getAccessControlList(AccessControlManager acm, String path)
+                       throws RepositoryException {
+               // search for an access control list
+               AccessControlList acl = null;
+               AccessControlPolicyIterator policyIterator = acm.getApplicablePolicies(path);
+               if (policyIterator.hasNext()) {
+                       while (policyIterator.hasNext()) {
+                               AccessControlPolicy acp = policyIterator.nextAccessControlPolicy();
+                               if (acp instanceof AccessControlList)
+                                       acl = ((AccessControlList) acp);
+                       }
+               } else {
+                       AccessControlPolicy[] existingPolicies = acm.getPolicies(path);
+                       for (AccessControlPolicy acp : existingPolicies) {
+                               if (acp instanceof AccessControlList)
+                                       acl = ((AccessControlList) acp);
+                       }
+               }
+               if (acl != null)
+                       return acl;
+               else
+                       throw new ArgeoJcrException("ACL not found at " + path);
+       }
+
+       /** Clear authorizations for a user at this path */
+       public synchronized static void clearAccessControList(Session session, String path, String username)
+                       throws RepositoryException {
+               AccessControlManager acm = session.getAccessControlManager();
+               AccessControlList acl = getAccessControlList(acm, path);
+               for (AccessControlEntry ace : acl.getAccessControlEntries()) {
+                       if (ace.getPrincipal().getName().equals(username)) {
+                               acl.removeAccessControlEntry(ace);
+                       }
+               }
+               // the new access control list must be applied otherwise this call:
+               // acl.removeAccessControlEntry(ace); has no effect
+               acm.setPolicy(path, acl);
+       }
+
+       /*
+        * FILES UTILITIES
+        */
+       /**
+        * Creates the nodes making the path as {@link NodeType#NT_FOLDER}
+        */
+       public static Node mkfolders(Session session, String path) {
+               return mkdirs(session, path, NodeType.NT_FOLDER, NodeType.NT_FOLDER, false);
+       }
+
+       /**
+        * Copy only nt:folder and nt:file, without their additional types and
+        * properties.
+        * 
+        * @param recursive
+        *            if true copies folders as well, otherwise only first level files
+        * @return how many files were copied
+        */
+       public static Long copyFiles(Node fromNode, Node toNode, Boolean recursive, JcrMonitor monitor, boolean onlyAdd) {
+               long count = 0l;
+
+               // Binary binary = null;
+               // InputStream in = null;
+               try {
+                       NodeIterator fromChildren = fromNode.getNodes();
+                       children: while (fromChildren.hasNext()) {
+                               if (monitor != null && monitor.isCanceled())
+                                       throw new ArgeoJcrException("Copy cancelled before it was completed");
+
+                               Node fromChild = fromChildren.nextNode();
+                               String fileName = fromChild.getName();
+                               if (fromChild.isNodeType(NodeType.NT_FILE)) {
+                                       if (onlyAdd && toNode.hasNode(fileName)) {
+                                               monitor.subTask("Skip existing " + fileName);
+                                               continue children;
+                                       }
+
+                                       if (monitor != null)
+                                               monitor.subTask("Copy " + fileName);
+                                       try (Bin binary = new Bin(fromChild.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA));
+                                                       InputStream in = binary.getStream();) {
+                                               copyStreamAsFile(toNode, fileName, in);
+                                       }
+                                       // IOUtils.closeQuietly(in);
+                                       // closeQuietly(binary);
+
+                                       // save session
+                                       toNode.getSession().save();
+                                       count++;
+
+                                       if (log.isDebugEnabled())
+                                               log.debug("Copied file " + fromChild.getPath());
+                                       if (monitor != null)
+                                               monitor.worked(1);
+                               } else if (fromChild.isNodeType(NodeType.NT_FOLDER) && recursive) {
+                                       Node toChildFolder;
+                                       if (toNode.hasNode(fileName)) {
+                                               toChildFolder = toNode.getNode(fileName);
+                                               if (!toChildFolder.isNodeType(NodeType.NT_FOLDER))
+                                                       throw new ArgeoJcrException(toChildFolder + " is not of type nt:folder");
+                                       } else {
+                                               toChildFolder = toNode.addNode(fileName, NodeType.NT_FOLDER);
+
+                                               // save session
+                                               toNode.getSession().save();
+                                       }
+                                       count = count + copyFiles(fromChild, toChildFolder, recursive, monitor, onlyAdd);
+                               }
+                       }
+                       return count;
+               } catch (RepositoryException | IOException e) {
+                       throw new ArgeoJcrException("Cannot copy files between " + fromNode + " and " + toNode);
+               } finally {
+                       // in case there was an exception
+                       // IOUtils.closeQuietly(in);
+                       // closeQuietly(binary);
+               }
+       }
+
+       /**
+        * Iteratively count all file nodes in subtree, inefficient but can be useful
+        * when query are poorly supported, such as in remoting.
+        */
+       public static Long countFiles(Node node) {
+               Long localCount = 0l;
+               try {
+                       for (NodeIterator nit = node.getNodes(); nit.hasNext();) {
+                               Node child = nit.nextNode();
+                               if (child.isNodeType(NodeType.NT_FOLDER))
+                                       localCount = localCount + countFiles(child);
+                               else if (child.isNodeType(NodeType.NT_FILE))
+                                       localCount = localCount + 1;
+                       }
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot count all children of " + node);
+               }
+               return localCount;
+       }
+
+       /**
+        * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session is
+        * NOT saved.
+        * 
+        * @return the created file node
+        */
+       public static Node copyFile(Node folderNode, File file) {
+               // InputStream in = null;
+               try (InputStream in = new FileInputStream(file)) {
+                       // in = new FileInputStream(file);
+                       return copyStreamAsFile(folderNode, file.getName(), in);
+               } catch (IOException e) {
+                       throw new ArgeoJcrException("Cannot copy file " + file + " under " + folderNode, e);
+                       // } finally {
+                       // IOUtils.closeQuietly(in);
+               }
+       }
+
+       /** Copy bytes as an nt:file */
+       public static Node copyBytesAsFile(Node folderNode, String fileName, byte[] bytes) {
+               // InputStream in = null;
+               try (InputStream in = new ByteArrayInputStream(bytes)) {
+                       // in = new ByteArrayInputStream(bytes);
+                       return copyStreamAsFile(folderNode, fileName, in);
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot copy file " + fileName + " under " + folderNode, e);
+                       // } finally {
+                       // IOUtils.closeQuietly(in);
+               }
+       }
+
+       /**
+        * Copy a stream as an nt:file, assuming an nt:folder hierarchy. The session is
+        * NOT saved.
+        * 
+        * @return the created file node
+        */
+       public static Node copyStreamAsFile(Node folderNode, String fileName, InputStream in) {
+               Binary binary = null;
+               try {
+                       Node fileNode;
+                       Node contentNode;
+                       if (folderNode.hasNode(fileName)) {
+                               fileNode = folderNode.getNode(fileName);
+                               if (!fileNode.isNodeType(NodeType.NT_FILE))
+                                       throw new ArgeoJcrException(fileNode + " is not of type nt:file");
+                               // we assume that the content node is already there
+                               contentNode = fileNode.getNode(Node.JCR_CONTENT);
+                       } else {
+                               fileNode = folderNode.addNode(fileName, NodeType.NT_FILE);
+                               contentNode = fileNode.addNode(Node.JCR_CONTENT, NodeType.NT_RESOURCE);
+                       }
+                       binary = contentNode.getSession().getValueFactory().createBinary(in);
+                       contentNode.setProperty(Property.JCR_DATA, binary);
+                       return fileNode;
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot create file node " + fileName + " under " + folderNode, e);
+               } finally {
+                       closeQuietly(binary);
+               }
+       }
+
+       /**
+        * Computes the checksum of an nt:file.
+        * 
+        * @deprecated use separate digest utilities
+        */
+       @Deprecated
+       public static String checksumFile(Node fileNode, String algorithm) {
+               Binary data = null;
+               try (InputStream in = fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary()
+                               .getStream()) {
+                       return digest(algorithm, in);
+               } catch (RepositoryException | IOException e) {
+                       throw new ArgeoJcrException("Cannot checksum file " + fileNode, e);
+               } finally {
+                       closeQuietly(data);
+               }
+       }
+
+       @Deprecated
+       private static String digest(String algorithm, InputStream in) {
+               final Integer byteBufferCapacity = 100 * 1024;// 100 KB
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(algorithm);
+                       byte[] buffer = new byte[byteBufferCapacity];
+                       int read = 0;
+                       while ((read = in.read(buffer)) > 0) {
+                               digest.update(buffer, 0, read);
+                       }
+
+                       byte[] checksum = digest.digest();
+                       String res = encodeHexString(checksum);
+                       return res;
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot digest with algorithm " + algorithm, e);
+               }
+       }
+
+       /**
+        * From
+        * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
+        * -a-hex-string-in-java
+        */
+       @Deprecated
+       private static String encodeHexString(byte[] bytes) {
+               final char[] hexArray = "0123456789abcdef".toCharArray();
+               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.jcr/src/org/argeo/jcr/PropertyDiff.java b/org.argeo.jcr/src/org/argeo/jcr/PropertyDiff.java
new file mode 100644 (file)
index 0000000..a0ff471
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import javax.jcr.Value;
+
+/** The result of the comparison of two JCR properties. */
+public class PropertyDiff {
+       public final static Integer MODIFIED = 0;
+       public final static Integer ADDED = 1;
+       public final static Integer REMOVED = 2;
+
+       private final Integer type;
+       private final String relPath;
+       private final Value referenceValue;
+       private final Value newValue;
+
+       public PropertyDiff(Integer type, String relPath, Value referenceValue,
+                       Value newValue) {
+               super();
+
+               if (type == MODIFIED) {
+                       if (referenceValue == null || newValue == null)
+                               throw new ArgeoJcrException(
+                                               "Reference and new values must be specified.");
+               } else if (type == ADDED) {
+                       if (referenceValue != null || newValue == null)
+                               throw new ArgeoJcrException(
+                                               "New value and only it must be specified.");
+               } else if (type == REMOVED) {
+                       if (referenceValue == null || newValue != null)
+                               throw new ArgeoJcrException(
+                                               "Reference value and only it must be specified.");
+               } else {
+                       throw new ArgeoJcrException("Unkown diff type " + type);
+               }
+
+               if (relPath == null)
+                       throw new ArgeoJcrException("Relative path must be specified");
+
+               this.type = type;
+               this.relPath = relPath;
+               this.referenceValue = referenceValue;
+               this.newValue = newValue;
+       }
+
+       public Integer getType() {
+               return type;
+       }
+
+       public String getRelPath() {
+               return relPath;
+       }
+
+       public Value getReferenceValue() {
+               return referenceValue;
+       }
+
+       public Value getNewValue() {
+               return newValue;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/SimplePrincipal.java b/org.argeo.jcr/src/org/argeo/jcr/SimplePrincipal.java
new file mode 100644 (file)
index 0000000..aee3196
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import java.security.Principal;
+
+/** Canonical implementation of a {@link Principal} */
+class SimplePrincipal implements Principal {
+       private final String name;
+
+       public SimplePrincipal(String name) {
+               if (name == null)
+                       throw new IllegalArgumentException("Principal name cannot be null");
+               this.name = name;
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       @Override
+       public int hashCode() {
+               return name.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj == null)
+                       return false;
+               if (obj instanceof Principal)
+                       return name.equals((((Principal) obj).getName()));
+               return name.equals(obj.toString());
+       }
+
+       @Override
+       protected Object clone() throws CloneNotSupportedException {
+               return new SimplePrincipal(name);
+       }
+
+       @Override
+       public String toString() {
+               return name;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java b/org.argeo.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java
new file mode 100644 (file)
index 0000000..281bd01
--- /dev/null
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.LoginException;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/** Proxy JCR sessions and attach them to calling threads. */
+@Deprecated
+public abstract class ThreadBoundJcrSessionFactory {
+       private final static Log log = LogFactory.getLog(ThreadBoundJcrSessionFactory.class);
+
+       private Repository repository;
+       /** can be injected as list, only used if repository is null */
+       private List<Repository> repositories;
+
+       private ThreadLocal<Session> session = new ThreadLocal<Session>();
+       private final Session proxiedSession;
+       /** If workspace is null, default will be used. */
+       private String workspace = null;
+
+       private String defaultUsername = "demo";
+       private String defaultPassword = "demo";
+       private Boolean forceDefaultCredentials = false;
+
+       private boolean active = true;
+
+       // monitoring
+       private final List<Thread> threads = Collections.synchronizedList(new ArrayList<Thread>());
+       private final Map<Long, Session> activeSessions = Collections.synchronizedMap(new HashMap<Long, Session>());
+       private MonitoringThread monitoringThread;
+
+       public ThreadBoundJcrSessionFactory() {
+               Class<?>[] interfaces = { Session.class };
+               proxiedSession = (Session) Proxy.newProxyInstance(ThreadBoundJcrSessionFactory.class.getClassLoader(),
+                               interfaces, new JcrSessionInvocationHandler());
+       }
+
+       /** Logs in to the repository using various strategies. */
+       protected synchronized Session login() {
+               if (!isActive())
+                       throw new ArgeoJcrException("Thread bound session factory inactive");
+
+               // discard session previously attached to this thread
+               Thread thread = Thread.currentThread();
+               if (activeSessions.containsKey(thread.getId())) {
+                       Session oldSession = activeSessions.remove(thread.getId());
+                       oldSession.logout();
+                       session.remove();
+               }
+
+               Session newSession = null;
+               // first try to login without credentials, assuming the underlying login
+               // module will have dealt with authentication (typically using Spring
+               // Security)
+               if (!forceDefaultCredentials)
+                       try {
+                               newSession = repository().login(workspace);
+                       } catch (LoginException e1) {
+                               log.warn("Cannot login without credentials: " + e1.getMessage());
+                               // invalid credentials, go to the next step
+                       } catch (RepositoryException e1) {
+                               // other kind of exception, fail
+                               throw new ArgeoJcrException("Cannot log in to repository", e1);
+                       }
+
+               // log using default username / password (useful for testing purposes)
+               if (newSession == null)
+                       try {
+                               SimpleCredentials sc = new SimpleCredentials(defaultUsername, defaultPassword.toCharArray());
+                               newSession = repository().login(sc, workspace);
+                       } catch (RepositoryException e) {
+                               throw new ArgeoJcrException("Cannot log in to repository", e);
+                       }
+
+               session.set(newSession);
+               // Log and monitor new session
+               if (log.isTraceEnabled())
+                       log.trace("Logged in to JCR session " + newSession + "; userId=" + newSession.getUserID());
+
+               // monitoring
+               activeSessions.put(thread.getId(), newSession);
+               threads.add(thread);
+               return newSession;
+       }
+
+       public Object getObject() {
+               return proxiedSession;
+       }
+
+       public void init() throws Exception {
+               // log.error("SHOULD NOT BE USED ANYMORE");
+               monitoringThread = new MonitoringThread();
+               monitoringThread.start();
+       }
+
+       public void dispose() throws Exception {
+               // if (activeSessions.size() == 0)
+               // return;
+
+               if (log.isTraceEnabled())
+                       log.trace("Cleaning up " + activeSessions.size() + " active JCR sessions...");
+
+               deactivate();
+               for (Session sess : activeSessions.values()) {
+                       JcrUtils.logoutQuietly(sess);
+               }
+               activeSessions.clear();
+       }
+
+       protected Boolean isActive() {
+               return active;
+       }
+
+       protected synchronized void deactivate() {
+               active = false;
+               notifyAll();
+       }
+
+       protected synchronized void removeSession(Thread thread) {
+               if (!isActive())
+                       return;
+               activeSessions.remove(thread.getId());
+               threads.remove(thread);
+       }
+
+       protected synchronized void cleanDeadThreads() {
+               if (!isActive())
+                       return;
+               Iterator<Thread> it = threads.iterator();
+               while (it.hasNext()) {
+                       Thread thread = it.next();
+                       if (!thread.isAlive() && isActive()) {
+                               if (activeSessions.containsKey(thread.getId())) {
+                                       Session session = activeSessions.get(thread.getId());
+                                       activeSessions.remove(thread.getId());
+                                       session.logout();
+                                       if (log.isTraceEnabled())
+                                               log.trace("Cleaned up JCR session (userID=" + session.getUserID() + ") from dead thread "
+                                                               + thread.getId());
+                               }
+                               it.remove();
+                       }
+               }
+               try {
+                       wait(1000);
+               } catch (InterruptedException e) {
+                       // silent
+               }
+       }
+
+       public Class<? extends Session> getObjectType() {
+               return Session.class;
+       }
+
+       public boolean isSingleton() {
+               return true;
+       }
+
+       /**
+        * Called before a method is actually called, allowing to check the session
+        * or re-login it (e.g. if authentication has changed). The default
+        * implementation returns the session.
+        */
+       protected Session preCall(Session session) {
+               return session;
+       }
+
+       protected Repository repository() {
+               if (repository != null)
+                       return repository;
+               if (repositories != null) {
+                       // hardened for OSGi dynamic services
+                       Iterator<Repository> it = repositories.iterator();
+                       if (it.hasNext())
+                               return it.next();
+               }
+               throw new ArgeoJcrException("No repository injected");
+       }
+
+       // /** Useful for declarative registration of OSGi services (blueprint) */
+       // public void register(Repository repository, Map<?, ?> params) {
+       // this.repository = repository;
+       // }
+       //
+       // /** Useful for declarative registration of OSGi services (blueprint) */
+       // public void unregister(Repository repository, Map<?, ?> params) {
+       // this.repository = null;
+       // }
+
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       public void setRepositories(List<Repository> repositories) {
+               this.repositories = repositories;
+       }
+
+       public void setDefaultUsername(String defaultUsername) {
+               this.defaultUsername = defaultUsername;
+       }
+
+       public void setDefaultPassword(String defaultPassword) {
+               this.defaultPassword = defaultPassword;
+       }
+
+       public void setForceDefaultCredentials(Boolean forceDefaultCredentials) {
+               this.forceDefaultCredentials = forceDefaultCredentials;
+       }
+
+       public void setWorkspace(String workspace) {
+               this.workspace = workspace;
+       }
+
+       protected class JcrSessionInvocationHandler implements InvocationHandler {
+
+               public Object invoke(Object proxy, Method method, Object[] args) throws Throwable, RepositoryException {
+                       Session threadSession = session.get();
+                       if (threadSession == null) {
+                               if ("logout".equals(method.getName()))// no need to login
+                                       return Void.TYPE;
+                               else if ("toString".equals(method.getName()))// maybe logging
+                                       return "Uninitialized Argeo thread bound JCR session";
+                               threadSession = login();
+                       }
+
+                       preCall(threadSession);
+                       Object ret;
+                       try {
+                               ret = method.invoke(threadSession, args);
+                       } catch (InvocationTargetException e) {
+                               Throwable cause = e.getCause();
+                               if (cause instanceof RepositoryException)
+                                       throw (RepositoryException) cause;
+                               else
+                                       throw cause;
+                       }
+                       if ("logout".equals(method.getName())) {
+                               session.remove();
+                               Thread thread = Thread.currentThread();
+                               removeSession(thread);
+                               if (log.isTraceEnabled())
+                                       log.trace("Logged out JCR session (userId=" + threadSession.getUserID() + ") on thread "
+                                                       + thread.getId());
+                       }
+                       return ret;
+               }
+       }
+
+       /** Monitors registered thread in order to clean up dead ones. */
+       private class MonitoringThread extends Thread {
+
+               public MonitoringThread() {
+                       super("ThreadBound JCR Session Monitor");
+               }
+
+               @Override
+               public void run() {
+                       while (isActive()) {
+                               cleanDeadThreads();
+                       }
+               }
+
+       }
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/VersionDiff.java b/org.argeo.jcr/src/org/argeo/jcr/VersionDiff.java
new file mode 100644 (file)
index 0000000..e6ae913
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr;
+
+import java.util.Calendar;
+import java.util.Map;
+
+/**
+ * Generic Object that enables the creation of history reports based on a JCR
+ * versionable node. userId and creation date are added to the map of
+ * PropertyDiff.
+ * 
+ * These two fields might be null
+ * 
+ */
+public class VersionDiff {
+
+       private String userId;
+       private Map<String, PropertyDiff> diffs;
+       private Calendar updateTime;
+
+       public VersionDiff(String userId, Calendar updateTime,
+                       Map<String, PropertyDiff> diffs) {
+               this.userId = userId;
+               this.updateTime = updateTime;
+               this.diffs = diffs;
+       }
+
+       public String getUserId() {
+               return userId;
+       }
+
+       public Map<String, PropertyDiff> getDiffs() {
+               return diffs;
+       }
+
+       public Calendar getUpdateTime() {
+               return updateTime;
+       }
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookModel.java b/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookModel.java
new file mode 100644 (file)
index 0000000..61a902d
--- /dev/null
@@ -0,0 +1,42 @@
+package org.argeo.jcr.docbook;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.jcr.ImportUUIDBehavior;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+public class DocBookModel {
+       private final static Log log = LogFactory.getLog(DocBookModel.class);
+       private Session session;
+
+       public DocBookModel(Session session) {
+               super();
+               this.session = session;
+       }
+
+       public void setSession(Session session) {
+               this.session = session;
+       }
+
+       public void importXml(String path, InputStream in)
+                       throws RepositoryException, IOException {
+               long begin = System.currentTimeMillis();
+               session.importXML(path, in,
+                               ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING);
+               long duration = System.currentTimeMillis() - begin;
+               if (log.isTraceEnabled())
+                       log.trace("Imported " + path + " in " + duration + " ms");
+
+       }
+
+       public void exportXml(String path, OutputStream out)
+                       throws RepositoryException, IOException {
+               session.exportDocumentView(path, out, true, false);
+       }
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookNames.java b/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookNames.java
new file mode 100644 (file)
index 0000000..291b30c
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.jcr.docbook;
+
+public interface DocBookNames {
+       // ELEMENTS
+       public final static String DBK_PARA = "dbk:para";
+       public final static String DBK_SECTION = "dbk:section";
+       public final static String DBK_MEDIAOBJECT = "dbk:mediaobject";
+
+       // ATTRIBUTES
+       // TODO centralise
+       public final static String JCR_XMLTEXT = "jcr:xmltext";
+       public final static String JCR_XMLCHARACTERS = "jcr:xmlcharacters";
+       
+       public final static String DBK_ROLE = "dbk:role";
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookTypes.java b/org.argeo.jcr/src/org/argeo/jcr/docbook/DocBookTypes.java
new file mode 100644 (file)
index 0000000..176c5c3
--- /dev/null
@@ -0,0 +1,12 @@
+package org.argeo.jcr.docbook;
+
+public interface DocBookTypes {
+       public final static String ARTICLE = "dbk:article";
+       public final static String SECTION = "dbk:section";
+       public final static String PARA = "dbk:para";
+       public final static String XMLTEXT = "dbk:xmltext";
+
+       public final static String MEDIAOBJECT = "dbk:mediaobject";
+       public final static String IMAGEOBJECT = "dbk:imageobject";
+       public final static String IMAGEDATA = "dbk:imagedata";
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/docbook/docbook-full.cnd b/org.argeo.jcr/src/org/argeo/jcr/docbook/docbook-full.cnd
new file mode 100644 (file)
index 0000000..7e4fefa
--- /dev/null
@@ -0,0 +1,3005 @@
+<dbk = 'http://docbook.org/ns/docbook'>
+<argeodbk = 'http://www.argeo.org/ns/argeodbk'>
+<jcr = 'http://www.jcp.org/jcr/1.0'>
+<nt = 'http://www.jcp.org/jcr/nt/1.0'>
+<xlink = 'http://www.w3.org/1999/xlink'>
+<xs = 'http://www.w3.org/2001/XMLSchema'>
+<xml = 'http://www.w3.org/XML/1998/namespace'>
+
+[argeodbk:titled]
+mixin
+ + dbk:info (dbk:info) = dbk:info *
+ + dbk:title (dbk:title) = dbk:title *
+ + dbk:titleabbrev (dbk:titleabbrev) = dbk:titleabbrev *
+
+[argeodbk:linkingAttributes]
+mixin
+ - dbk:linkend (String)
+ - xlink:actuate (String)
+ - xlink:arcrole (String)
+ - xlink:href (String)
+ - xlink:role (String)
+ - xlink:show (String)
+ - xlink:title (String)
+ - xlink:type (String)
+
+[argeodbk:freeText]
+mixin
+ + dbk:phrase (dbk:phrase) = dbk:phrase *
+ + dbk:replaceable (dbk:replaceable) = dbk:replaceable *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[argeodbk:markupInlines]
+mixin
+ + dbk:code (dbk:code) = dbk:code *
+ + dbk:constant (dbk:constant) = dbk:constant *
+ + dbk:email (dbk:email) = dbk:email *
+ + dbk:literal (dbk:literal) = dbk:literal *
+ + dbk:markup (dbk:markup) = dbk:markup *
+ + dbk:symbol (dbk:symbol) = dbk:symbol *
+ + dbk:tag (dbk:tag) = dbk:tag *
+ + dbk:token (dbk:token) = dbk:token *
+ + dbk:uri (dbk:uri) = dbk:uri *
+
+[argeodbk:listElements]
+mixin
+ + dbk:bibliolist (dbk:bibliolist) = dbk:bibliolist *
+ + dbk:calloutlist (dbk:calloutlist) = dbk:calloutlist *
+ + dbk:glosslist (dbk:glosslist) = dbk:glosslist *
+ + dbk:itemizedlist (dbk:itemizedlist) = dbk:itemizedlist *
+ + dbk:orderedlist (dbk:orderedlist) = dbk:orderedlist *
+ + dbk:procedure (dbk:procedure) = dbk:procedure *
+ + dbk:qandaset (dbk:qandaset) = dbk:qandaset *
+ + dbk:segmentedlist (dbk:segmentedlist) = dbk:segmentedlist *
+ + dbk:simplelist (dbk:simplelist) = dbk:simplelist *
+ + dbk:variablelist (dbk:variablelist) = dbk:variablelist *
+
+[argeodbk:paragraphElements]
+mixin
+ + dbk:formalpara (dbk:formalpara) = dbk:formalpara *
+ + dbk:para (dbk:para) = dbk:para *
+ + dbk:simpara (dbk:simpara) = dbk:simpara *
+
+[argeodbk:indexingInlines]
+mixin
+ + dbk:indexterm (dbk:indexterm) = dbk:indexterm *
+
+[argeodbk:techDocElements]
+mixin
+ + dbk:caution (dbk:caution) = dbk:caution *
+ + dbk:classsynopsis (dbk:classsynopsis) = dbk:classsynopsis *
+ + dbk:cmdsynopsis (dbk:cmdsynopsis) = dbk:cmdsynopsis *
+ + dbk:constraintdef (dbk:constraintdef) = dbk:constraintdef *
+ + dbk:constructorsynopsis (dbk:constructorsynopsis) = dbk:constructorsynopsis *
+ + dbk:destructorsynopsis (dbk:destructorsynopsis) = dbk:destructorsynopsis *
+ + dbk:equation (dbk:equation) = dbk:equation *
+ + dbk:example (dbk:example) = dbk:example *
+ + dbk:fieldsynopsis (dbk:fieldsynopsis) = dbk:fieldsynopsis *
+ + dbk:figure (dbk:figure) = dbk:figure *
+ + dbk:funcsynopsis (dbk:funcsynopsis) = dbk:funcsynopsis *
+ + dbk:important (dbk:important) = dbk:important *
+ + dbk:informalequation (dbk:informalequation) = dbk:informalequation *
+ + dbk:informalexample (dbk:informalexample) = dbk:informalexample *
+ + dbk:informalfigure (dbk:informalfigure) = dbk:informalfigure *
+ + dbk:informaltable (dbk:informaltable) = dbk:informaltable *
+ + dbk:literallayout (dbk:literallayout) = dbk:literallayout *
+ + dbk:methodsynopsis (dbk:methodsynopsis) = dbk:methodsynopsis *
+ + dbk:msgset (dbk:msgset) = dbk:msgset *
+ + dbk:note (dbk:note) = dbk:note *
+ + dbk:productionset (dbk:productionset) = dbk:productionset *
+ + dbk:programlisting (dbk:programlisting) = dbk:programlisting *
+ + dbk:programlistingco (dbk:programlistingco) = dbk:programlistingco *
+ + dbk:screen (dbk:screen) = dbk:screen *
+ + dbk:screenco (dbk:screenco) = dbk:screenco *
+ + dbk:synopsis (dbk:synopsis) = dbk:synopsis *
+ + dbk:table (dbk:table) = dbk:table *
+ + dbk:task (dbk:task) = dbk:task *
+ + dbk:tip (dbk:tip) = dbk:tip *
+ + dbk:warning (dbk:warning) = dbk:warning *
+
+[argeodbk:techDocInlines]
+mixin
+ + dbk:accel (dbk:accel) = dbk:accel *
+ + dbk:application (dbk:application) = dbk:application *
+ + dbk:classname (dbk:classname) = dbk:classname *
+ + dbk:command (dbk:command) = dbk:command *
+ + dbk:computeroutput (dbk:computeroutput) = dbk:computeroutput *
+ + dbk:database (dbk:database) = dbk:database *
+ + dbk:envar (dbk:envar) = dbk:envar *
+ + dbk:errorcode (dbk:errorcode) = dbk:errorcode *
+ + dbk:errorname (dbk:errorname) = dbk:errorname *
+ + dbk:errortext (dbk:errortext) = dbk:errortext *
+ + dbk:errortype (dbk:errortype) = dbk:errortype *
+ + dbk:exceptionname (dbk:exceptionname) = dbk:exceptionname *
+ + dbk:filename (dbk:filename) = dbk:filename *
+ + dbk:function (dbk:function) = dbk:function *
+ + dbk:guibutton (dbk:guibutton) = dbk:guibutton *
+ + dbk:guiicon (dbk:guiicon) = dbk:guiicon *
+ + dbk:guilabel (dbk:guilabel) = dbk:guilabel *
+ + dbk:guimenu (dbk:guimenu) = dbk:guimenu *
+ + dbk:guimenuitem (dbk:guimenuitem) = dbk:guimenuitem *
+ + dbk:guisubmenu (dbk:guisubmenu) = dbk:guisubmenu *
+ + dbk:hardware (dbk:hardware) = dbk:hardware *
+ + dbk:initializer (dbk:initializer) = dbk:initializer *
+ + dbk:inlineequation (dbk:inlineequation) = dbk:inlineequation *
+ + dbk:interfacename (dbk:interfacename) = dbk:interfacename *
+ + dbk:keycap (dbk:keycap) = dbk:keycap *
+ + dbk:keycode (dbk:keycode) = dbk:keycode *
+ + dbk:keycombo (dbk:keycombo) = dbk:keycombo *
+ + dbk:keysym (dbk:keysym) = dbk:keysym *
+ + dbk:menuchoice (dbk:menuchoice) = dbk:menuchoice *
+ + dbk:methodname (dbk:methodname) = dbk:methodname *
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:mousebutton (dbk:mousebutton) = dbk:mousebutton *
+ + dbk:nonterminal (dbk:nonterminal) = dbk:nonterminal *
+ + dbk:ooclass (dbk:ooclass) = dbk:ooclass *
+ + dbk:ooexception (dbk:ooexception) = dbk:ooexception *
+ + dbk:oointerface (dbk:oointerface) = dbk:oointerface *
+ + dbk:option (dbk:option) = dbk:option *
+ + dbk:optional (dbk:optional) = dbk:optional *
+ + dbk:package (dbk:package) = dbk:package *
+ + dbk:parameter (dbk:parameter) = dbk:parameter *
+ + dbk:productname (dbk:productname) = dbk:productname *
+ + dbk:productnumber (dbk:productnumber) = dbk:productnumber *
+ + dbk:prompt (dbk:prompt) = dbk:prompt *
+ + dbk:property (dbk:property) = dbk:property *
+ + dbk:returnvalue (dbk:returnvalue) = dbk:returnvalue *
+ + dbk:shortcut (dbk:shortcut) = dbk:shortcut *
+ + dbk:systemitem (dbk:systemitem) = dbk:systemitem *
+ + dbk:termdef (dbk:termdef) = dbk:termdef *
+ + dbk:trademark (dbk:trademark) = dbk:trademark *
+ + dbk:type (dbk:type) = dbk:type *
+ + dbk:userinput (dbk:userinput) = dbk:userinput *
+ + dbk:varname (dbk:varname) = dbk:varname *
+
+[argeodbk:publishingElements]
+mixin
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:blockquote (dbk:blockquote) = dbk:blockquote *
+ + dbk:epigraph (dbk:epigraph) = dbk:epigraph *
+ + dbk:sidebar (dbk:sidebar) = dbk:sidebar *
+
+[argeodbk:ubiquitousInlines]
+mixin
+ + dbk:alt (dbk:alt) = dbk:alt *
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:biblioref (dbk:biblioref) = dbk:biblioref *
+ + dbk:inlinemediaobject (dbk:inlinemediaobject) = dbk:inlinemediaobject *
+ + dbk:link (dbk:link) = dbk:link *
+ + dbk:olink (dbk:olink) = dbk:olink *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:subscript (dbk:subscript) = dbk:subscript *
+ + dbk:superscript (dbk:superscript) = dbk:superscript *
+ + dbk:xref (dbk:xref) = dbk:xref *
+
+[argeodbk:abstractSection]
+mixin
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bibliography (dbk:bibliography) = dbk:bibliography *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:glossary (dbk:glossary) = dbk:glossary *
+ + dbk:index (dbk:index) = dbk:index *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:toc (dbk:toc) = dbk:toc *
+ - dbk:label (String)
+ - dbk:status (String)
+
+[argeodbk:bibliographyInlines]
+mixin
+ + dbk:author (dbk:author) = dbk:author *
+ + dbk:citation (dbk:citation) = dbk:citation *
+ + dbk:citebiblioid (dbk:citebiblioid) = dbk:citebiblioid *
+ + dbk:citerefentry (dbk:citerefentry) = dbk:citerefentry *
+ + dbk:citetitle (dbk:citetitle) = dbk:citetitle *
+ + dbk:editor (dbk:editor) = dbk:editor *
+ + dbk:jobtitle (dbk:jobtitle) = dbk:jobtitle *
+ + dbk:org (dbk:org) = dbk:org *
+ + dbk:orgname (dbk:orgname) = dbk:orgname *
+ + dbk:person (dbk:person) = dbk:person *
+ + dbk:personname (dbk:personname) = dbk:personname *
+
+[argeodbk:publishingInlines]
+mixin
+ + dbk:abbrev (dbk:abbrev) = dbk:abbrev *
+ + dbk:acronym (dbk:acronym) = dbk:acronym *
+ + dbk:coref (dbk:coref) = dbk:coref *
+ + dbk:date (dbk:date) = dbk:date *
+ + dbk:emphasis (dbk:emphasis) = dbk:emphasis *
+ + dbk:firstterm (dbk:firstterm) = dbk:firstterm *
+ + dbk:footnote (dbk:footnote) = dbk:footnote *
+ + dbk:footnoteref (dbk:footnoteref) = dbk:footnoteref *
+ + dbk:foreignphrase (dbk:foreignphrase) = dbk:foreignphrase *
+ + dbk:glossterm (dbk:glossterm) = dbk:glossterm *
+ + dbk:quote (dbk:quote) = dbk:quote *
+ + dbk:wordasword (dbk:wordasword) = dbk:wordasword *
+
+[argeodbk:base]
+abstract
+ - dbk:annotations (String)
+ - dbk:arch (String)
+ - dbk:audience (String)
+ - dbk:condition (String)
+ - dbk:conformance (String)
+ - dbk:dir (String)
+ - dbk:os (String)
+ - dbk:remap (String)
+ - dbk:revision (String)
+ - dbk:revisionflag (String)
+ - dbk:role (String)
+ - dbk:security (String)
+ - dbk:userlevel (String)
+ - dbk:vendor (String)
+ - dbk:version (String)
+ - dbk:wordsize (String)
+ - dbk:xreflabel (String)
+ - xml:base (String)
+ - xml:id (String)
+ - xml:lang (String)
+
+[dbk:abbrev] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:trademark (dbk:trademark) = dbk:trademark *
+
+[dbk:abstract] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:paragraphElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+
+[dbk:accel] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:acknowledgements] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:acronym] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:trademark (dbk:trademark) = dbk:trademark *
+
+[dbk:address] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:city (dbk:city) = dbk:city *
+ + dbk:country (dbk:country) = dbk:country *
+ + dbk:email (dbk:email) = dbk:email *
+ + dbk:fax (dbk:fax) = dbk:fax *
+ + dbk:otheraddr (dbk:otheraddr) = dbk:otheraddr *
+ + dbk:personname (dbk:personname) = dbk:personname *
+ + dbk:phone (dbk:phone) = dbk:phone *
+ + dbk:pob (dbk:pob) = dbk:pob *
+ + dbk:postcode (dbk:postcode) = dbk:postcode *
+ + dbk:state (dbk:state) = dbk:state *
+ + dbk:street (dbk:street) = dbk:street *
+ + dbk:uri (dbk:uri) = dbk:uri *
+ - dbk:continuation (String) 
+ - dbk:language (String) 
+ - dbk:linenumbering (String) 
+ - dbk:startinglinenumber (String) 
+ - xml:space (String) 
+
+[dbk:affiliation] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:jobtitle (dbk:jobtitle) = dbk:jobtitle *
+ + dbk:org (dbk:org) = dbk:org
+ + dbk:orgdiv (dbk:orgdiv) = dbk:orgdiv *
+ + dbk:orgname (dbk:orgname) = dbk:orgname
+ + dbk:shortaffil (dbk:shortaffil) = dbk:shortaffil
+
+[dbk:alt] > argeodbk:base
+ + dbk:inlinemediaobject (dbk:inlinemediaobject) = dbk:inlinemediaobject *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:anchor] > argeodbk:base
+
+[dbk:annotation] > argeodbk:base, argeodbk:indexingInlines, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:annotates (String) 
+
+[dbk:answer] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:label (dbk:label) = dbk:label
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:appendix] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:refentry (dbk:refentry) = dbk:refentry *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:sect1 (dbk:sect1) = dbk:sect1 *
+ + dbk:section (dbk:section) = dbk:section *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+
+[dbk:application] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+
+[dbk:arc] > argeodbk:base
+ - xlink:from (String) 
+ - xlink:to (String) 
+
+[dbk:area] > argeodbk:base
+ + dbk:alt (dbk:alt) = dbk:alt
+ - dbk:coords (String) 
+ - dbk:label (String) 
+ - dbk:linkends (String) 
+ - dbk:otherunits (String) 
+ - dbk:units (String) 
+
+[dbk:areaset] > argeodbk:base
+ + dbk:area (dbk:area) = dbk:area *
+ - dbk:label (String) 
+ - dbk:linkends (String) 
+ - dbk:otherunits (String) 
+ - dbk:units (String) 
+
+[dbk:areaspec] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:area (dbk:area) = dbk:area *
+ + dbk:areaset (dbk:areaset) = dbk:areaset *
+ - dbk:otherunits (String) 
+ - dbk:units (String) 
+
+[dbk:arg] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:arg (dbk:arg) = dbk:arg *
+ + dbk:group (dbk:group) = dbk:group *
+ + dbk:option (dbk:option) = dbk:option *
+ + dbk:sbr (dbk:sbr) = dbk:sbr *
+ + dbk:synopfragmentref (dbk:synopfragmentref) = dbk:synopfragmentref *
+ - dbk:choice (String) 
+ - dbk:rep (String) 
+
+[dbk:article] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:acknowledgements (dbk:acknowledgements) = dbk:acknowledgements *
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:appendix (dbk:appendix) = dbk:appendix *
+ + dbk:colophon (dbk:colophon) = dbk:colophon *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:refentry (dbk:refentry) = dbk:refentry *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:sect1 (dbk:sect1) = dbk:sect1 *
+ + dbk:section (dbk:section) = dbk:section *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ - dbk:class (String) 
+
+[dbk:artpagenums] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:attribution] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:citation (dbk:citation) = dbk:citation *
+ + dbk:citetitle (dbk:citetitle) = dbk:citetitle *
+ + dbk:person (dbk:person) = dbk:person *
+ + dbk:personname (dbk:personname) = dbk:personname *
+
+[dbk:audiodata] > argeodbk:base
+ + dbk:info (dbk:info) = dbk:info
+ - dbk:entityref (String) 
+ - dbk:fileref (String) 
+ - dbk:format (String) 
+
+[dbk:audioobject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:audiodata (dbk:audiodata) = dbk:audiodata
+ + dbk:info (dbk:info) = dbk:info
+
+[dbk:author] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:affiliation (dbk:affiliation) = dbk:affiliation *
+ + dbk:contrib (dbk:contrib) = dbk:contrib *
+ + dbk:email (dbk:email) = dbk:email *
+ + dbk:orgdiv (dbk:orgdiv) = dbk:orgdiv *
+ + dbk:orgname (dbk:orgname) = dbk:orgname
+ + dbk:personblurb (dbk:personblurb) = dbk:personblurb *
+ + dbk:personname (dbk:personname) = dbk:personname
+ + dbk:uri (dbk:uri) = dbk:uri *
+
+[dbk:authorgroup] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:author (dbk:author) = dbk:author *
+ + dbk:editor (dbk:editor) = dbk:editor *
+ + dbk:othercredit (dbk:othercredit) = dbk:othercredit *
+
+[dbk:authorinitials] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:bibliocoverage] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:otherspatial (String) 
+ - dbk:othertemporal (String) 
+ - dbk:spatial (String) 
+ - dbk:temporal (String) 
+
+[dbk:bibliodiv] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:biblioentry (dbk:biblioentry) = dbk:biblioentry *
+ + dbk:bibliomixed (dbk:bibliomixed) = dbk:bibliomixed *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:biblioentry] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:publishingInlines
+ + dbk:abstract (dbk:abstract) = dbk:abstract *
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:artpagenums (dbk:artpagenums) = dbk:artpagenums *
+ + dbk:author (dbk:author) = dbk:author *
+ + dbk:authorgroup (dbk:authorgroup) = dbk:authorgroup *
+ + dbk:authorinitials (dbk:authorinitials) = dbk:authorinitials *
+ + dbk:bibliocoverage (dbk:bibliocoverage) = dbk:bibliocoverage *
+ + dbk:biblioid (dbk:biblioid) = dbk:biblioid *
+ + dbk:bibliomisc (dbk:bibliomisc) = dbk:bibliomisc *
+ + dbk:bibliomset (dbk:bibliomset) = dbk:bibliomset *
+ + dbk:bibliorelation (dbk:bibliorelation) = dbk:bibliorelation *
+ + dbk:biblioset (dbk:biblioset) = dbk:biblioset *
+ + dbk:bibliosource (dbk:bibliosource) = dbk:bibliosource *
+ + dbk:citebiblioid (dbk:citebiblioid) = dbk:citebiblioid *
+ + dbk:citerefentry (dbk:citerefentry) = dbk:citerefentry *
+ + dbk:citetitle (dbk:citetitle) = dbk:citetitle *
+ + dbk:collab (dbk:collab) = dbk:collab *
+ + dbk:confgroup (dbk:confgroup) = dbk:confgroup *
+ + dbk:contractnum (dbk:contractnum) = dbk:contractnum *
+ + dbk:contractsponsor (dbk:contractsponsor) = dbk:contractsponsor *
+ + dbk:copyright (dbk:copyright) = dbk:copyright *
+ + dbk:cover (dbk:cover) = dbk:cover *
+ + dbk:edition (dbk:edition) = dbk:edition *
+ + dbk:editor (dbk:editor) = dbk:editor *
+ + dbk:extendedlink (dbk:extendedlink) = dbk:extendedlink *
+ + dbk:issuenum (dbk:issuenum) = dbk:issuenum *
+ + dbk:itermset (dbk:itermset) = dbk:itermset *
+ + dbk:keywordset (dbk:keywordset) = dbk:keywordset *
+ + dbk:legalnotice (dbk:legalnotice) = dbk:legalnotice *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:org (dbk:org) = dbk:org *
+ + dbk:orgname (dbk:orgname) = dbk:orgname *
+ + dbk:othercredit (dbk:othercredit) = dbk:othercredit *
+ + dbk:pagenums (dbk:pagenums) = dbk:pagenums *
+ + dbk:person (dbk:person) = dbk:person *
+ + dbk:personblurb (dbk:personblurb) = dbk:personblurb *
+ + dbk:personname (dbk:personname) = dbk:personname *
+ + dbk:phrase (dbk:phrase) = dbk:phrase *
+ + dbk:printhistory (dbk:printhistory) = dbk:printhistory *
+ + dbk:productname (dbk:productname) = dbk:productname *
+ + dbk:productnumber (dbk:productnumber) = dbk:productnumber *
+ + dbk:pubdate (dbk:pubdate) = dbk:pubdate *
+ + dbk:publisher (dbk:publisher) = dbk:publisher *
+ + dbk:publishername (dbk:publishername) = dbk:publishername *
+ + dbk:releaseinfo (dbk:releaseinfo) = dbk:releaseinfo *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:seriesvolnums (dbk:seriesvolnums) = dbk:seriesvolnums *
+ + dbk:subjectset (dbk:subjectset) = dbk:subjectset *
+ + dbk:subscript (dbk:subscript) = dbk:subscript *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:superscript (dbk:superscript) = dbk:superscript *
+ + dbk:title (dbk:title) = dbk:title *
+ + dbk:titleabbrev (dbk:titleabbrev) = dbk:titleabbrev *
+ + dbk:volumenum (dbk:volumenum) = dbk:volumenum *
+
+[dbk:bibliography] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bibliodiv (dbk:bibliodiv) = dbk:bibliodiv *
+ + dbk:biblioentry (dbk:biblioentry) = dbk:biblioentry *
+ + dbk:bibliomixed (dbk:bibliomixed) = dbk:bibliomixed *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:biblioid] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+ - dbk:otherclass (String) 
+
+[dbk:bibliolist] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:biblioentry (dbk:biblioentry) = dbk:biblioentry *
+ + dbk:bibliomixed (dbk:bibliomixed) = dbk:bibliomixed *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:bibliomisc] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:bibliomixed] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:publishingInlines
+ + dbk:abstract (dbk:abstract) = dbk:abstract *
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:artpagenums (dbk:artpagenums) = dbk:artpagenums *
+ + dbk:author (dbk:author) = dbk:author *
+ + dbk:authorgroup (dbk:authorgroup) = dbk:authorgroup *
+ + dbk:authorinitials (dbk:authorinitials) = dbk:authorinitials *
+ + dbk:bibliocoverage (dbk:bibliocoverage) = dbk:bibliocoverage *
+ + dbk:biblioid (dbk:biblioid) = dbk:biblioid *
+ + dbk:bibliomisc (dbk:bibliomisc) = dbk:bibliomisc *
+ + dbk:bibliomset (dbk:bibliomset) = dbk:bibliomset *
+ + dbk:bibliorelation (dbk:bibliorelation) = dbk:bibliorelation *
+ + dbk:biblioset (dbk:biblioset) = dbk:biblioset *
+ + dbk:bibliosource (dbk:bibliosource) = dbk:bibliosource *
+ + dbk:citebiblioid (dbk:citebiblioid) = dbk:citebiblioid *
+ + dbk:citerefentry (dbk:citerefentry) = dbk:citerefentry *
+ + dbk:citetitle (dbk:citetitle) = dbk:citetitle *
+ + dbk:collab (dbk:collab) = dbk:collab *
+ + dbk:confgroup (dbk:confgroup) = dbk:confgroup *
+ + dbk:contractnum (dbk:contractnum) = dbk:contractnum *
+ + dbk:contractsponsor (dbk:contractsponsor) = dbk:contractsponsor *
+ + dbk:copyright (dbk:copyright) = dbk:copyright *
+ + dbk:cover (dbk:cover) = dbk:cover *
+ + dbk:edition (dbk:edition) = dbk:edition *
+ + dbk:editor (dbk:editor) = dbk:editor *
+ + dbk:extendedlink (dbk:extendedlink) = dbk:extendedlink *
+ + dbk:issuenum (dbk:issuenum) = dbk:issuenum *
+ + dbk:itermset (dbk:itermset) = dbk:itermset *
+ + dbk:keywordset (dbk:keywordset) = dbk:keywordset *
+ + dbk:legalnotice (dbk:legalnotice) = dbk:legalnotice *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:org (dbk:org) = dbk:org *
+ + dbk:orgname (dbk:orgname) = dbk:orgname *
+ + dbk:othercredit (dbk:othercredit) = dbk:othercredit *
+ + dbk:pagenums (dbk:pagenums) = dbk:pagenums *
+ + dbk:person (dbk:person) = dbk:person *
+ + dbk:personblurb (dbk:personblurb) = dbk:personblurb *
+ + dbk:personname (dbk:personname) = dbk:personname *
+ + dbk:phrase (dbk:phrase) = dbk:phrase *
+ + dbk:printhistory (dbk:printhistory) = dbk:printhistory *
+ + dbk:productname (dbk:productname) = dbk:productname *
+ + dbk:productnumber (dbk:productnumber) = dbk:productnumber *
+ + dbk:pubdate (dbk:pubdate) = dbk:pubdate *
+ + dbk:publisher (dbk:publisher) = dbk:publisher *
+ + dbk:publishername (dbk:publishername) = dbk:publishername *
+ + dbk:releaseinfo (dbk:releaseinfo) = dbk:releaseinfo *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:seriesvolnums (dbk:seriesvolnums) = dbk:seriesvolnums *
+ + dbk:subjectset (dbk:subjectset) = dbk:subjectset *
+ + dbk:subscript (dbk:subscript) = dbk:subscript *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:superscript (dbk:superscript) = dbk:superscript *
+ + dbk:title (dbk:title) = dbk:title *
+ + dbk:titleabbrev (dbk:titleabbrev) = dbk:titleabbrev *
+ + dbk:volumenum (dbk:volumenum) = dbk:volumenum *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:bibliomset] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:publishingInlines, argeodbk:ubiquitousInlines
+ + dbk:abstract (dbk:abstract) = dbk:abstract *
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:artpagenums (dbk:artpagenums) = dbk:artpagenums *
+ + dbk:author (dbk:author) = dbk:author *
+ + dbk:authorgroup (dbk:authorgroup) = dbk:authorgroup *
+ + dbk:authorinitials (dbk:authorinitials) = dbk:authorinitials *
+ + dbk:bibliocoverage (dbk:bibliocoverage) = dbk:bibliocoverage *
+ + dbk:biblioid (dbk:biblioid) = dbk:biblioid *
+ + dbk:bibliomisc (dbk:bibliomisc) = dbk:bibliomisc *
+ + dbk:bibliomset (dbk:bibliomset) = dbk:bibliomset *
+ + dbk:bibliorelation (dbk:bibliorelation) = dbk:bibliorelation *
+ + dbk:biblioset (dbk:biblioset) = dbk:biblioset *
+ + dbk:bibliosource (dbk:bibliosource) = dbk:bibliosource *
+ + dbk:citebiblioid (dbk:citebiblioid) = dbk:citebiblioid *
+ + dbk:citerefentry (dbk:citerefentry) = dbk:citerefentry *
+ + dbk:citetitle (dbk:citetitle) = dbk:citetitle *
+ + dbk:collab (dbk:collab) = dbk:collab *
+ + dbk:confgroup (dbk:confgroup) = dbk:confgroup *
+ + dbk:contractnum (dbk:contractnum) = dbk:contractnum *
+ + dbk:contractsponsor (dbk:contractsponsor) = dbk:contractsponsor *
+ + dbk:copyright (dbk:copyright) = dbk:copyright *
+ + dbk:cover (dbk:cover) = dbk:cover *
+ + dbk:edition (dbk:edition) = dbk:edition *
+ + dbk:editor (dbk:editor) = dbk:editor *
+ + dbk:extendedlink (dbk:extendedlink) = dbk:extendedlink *
+ + dbk:issuenum (dbk:issuenum) = dbk:issuenum *
+ + dbk:itermset (dbk:itermset) = dbk:itermset *
+ + dbk:keywordset (dbk:keywordset) = dbk:keywordset *
+ + dbk:legalnotice (dbk:legalnotice) = dbk:legalnotice *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:org (dbk:org) = dbk:org *
+ + dbk:orgname (dbk:orgname) = dbk:orgname *
+ + dbk:othercredit (dbk:othercredit) = dbk:othercredit *
+ + dbk:pagenums (dbk:pagenums) = dbk:pagenums *
+ + dbk:person (dbk:person) = dbk:person *
+ + dbk:personblurb (dbk:personblurb) = dbk:personblurb *
+ + dbk:personname (dbk:personname) = dbk:personname *
+ + dbk:printhistory (dbk:printhistory) = dbk:printhistory *
+ + dbk:productname (dbk:productname) = dbk:productname *
+ + dbk:productnumber (dbk:productnumber) = dbk:productnumber *
+ + dbk:pubdate (dbk:pubdate) = dbk:pubdate *
+ + dbk:publisher (dbk:publisher) = dbk:publisher *
+ + dbk:publishername (dbk:publishername) = dbk:publishername *
+ + dbk:releaseinfo (dbk:releaseinfo) = dbk:releaseinfo *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:seriesvolnums (dbk:seriesvolnums) = dbk:seriesvolnums *
+ + dbk:subjectset (dbk:subjectset) = dbk:subjectset *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:title (dbk:title) = dbk:title *
+ + dbk:titleabbrev (dbk:titleabbrev) = dbk:titleabbrev *
+ + dbk:volumenum (dbk:volumenum) = dbk:volumenum *
+ - dbk:relation (String) 
+
+[dbk:biblioref] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:begin (String) 
+ - dbk:end (String) 
+ - dbk:endterm (Reference) 
+ - dbk:units (String) 
+ - dbk:xrefstyle (String) 
+
+[dbk:bibliorelation] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+ - dbk:otherclass (String) 
+ - dbk:othertype (String) 
+ - dbk:type (String) 
+
+[dbk:biblioset] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:publishingInlines
+ + dbk:abstract (dbk:abstract) = dbk:abstract *
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:artpagenums (dbk:artpagenums) = dbk:artpagenums *
+ + dbk:author (dbk:author) = dbk:author *
+ + dbk:authorgroup (dbk:authorgroup) = dbk:authorgroup *
+ + dbk:authorinitials (dbk:authorinitials) = dbk:authorinitials *
+ + dbk:bibliocoverage (dbk:bibliocoverage) = dbk:bibliocoverage *
+ + dbk:biblioid (dbk:biblioid) = dbk:biblioid *
+ + dbk:bibliomisc (dbk:bibliomisc) = dbk:bibliomisc *
+ + dbk:bibliomset (dbk:bibliomset) = dbk:bibliomset *
+ + dbk:bibliorelation (dbk:bibliorelation) = dbk:bibliorelation *
+ + dbk:biblioset (dbk:biblioset) = dbk:biblioset *
+ + dbk:bibliosource (dbk:bibliosource) = dbk:bibliosource *
+ + dbk:citebiblioid (dbk:citebiblioid) = dbk:citebiblioid *
+ + dbk:citerefentry (dbk:citerefentry) = dbk:citerefentry *
+ + dbk:citetitle (dbk:citetitle) = dbk:citetitle *
+ + dbk:collab (dbk:collab) = dbk:collab *
+ + dbk:confgroup (dbk:confgroup) = dbk:confgroup *
+ + dbk:contractnum (dbk:contractnum) = dbk:contractnum *
+ + dbk:contractsponsor (dbk:contractsponsor) = dbk:contractsponsor *
+ + dbk:copyright (dbk:copyright) = dbk:copyright *
+ + dbk:cover (dbk:cover) = dbk:cover *
+ + dbk:edition (dbk:edition) = dbk:edition *
+ + dbk:editor (dbk:editor) = dbk:editor *
+ + dbk:extendedlink (dbk:extendedlink) = dbk:extendedlink *
+ + dbk:issuenum (dbk:issuenum) = dbk:issuenum *
+ + dbk:itermset (dbk:itermset) = dbk:itermset *
+ + dbk:keywordset (dbk:keywordset) = dbk:keywordset *
+ + dbk:legalnotice (dbk:legalnotice) = dbk:legalnotice *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:org (dbk:org) = dbk:org *
+ + dbk:orgname (dbk:orgname) = dbk:orgname *
+ + dbk:othercredit (dbk:othercredit) = dbk:othercredit *
+ + dbk:pagenums (dbk:pagenums) = dbk:pagenums *
+ + dbk:person (dbk:person) = dbk:person *
+ + dbk:personblurb (dbk:personblurb) = dbk:personblurb *
+ + dbk:personname (dbk:personname) = dbk:personname *
+ + dbk:phrase (dbk:phrase) = dbk:phrase *
+ + dbk:printhistory (dbk:printhistory) = dbk:printhistory *
+ + dbk:productname (dbk:productname) = dbk:productname *
+ + dbk:productnumber (dbk:productnumber) = dbk:productnumber *
+ + dbk:pubdate (dbk:pubdate) = dbk:pubdate *
+ + dbk:publisher (dbk:publisher) = dbk:publisher *
+ + dbk:publishername (dbk:publishername) = dbk:publishername *
+ + dbk:releaseinfo (dbk:releaseinfo) = dbk:releaseinfo *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:seriesvolnums (dbk:seriesvolnums) = dbk:seriesvolnums *
+ + dbk:subjectset (dbk:subjectset) = dbk:subjectset *
+ + dbk:subscript (dbk:subscript) = dbk:subscript *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:superscript (dbk:superscript) = dbk:superscript *
+ + dbk:title (dbk:title) = dbk:title *
+ + dbk:titleabbrev (dbk:titleabbrev) = dbk:titleabbrev *
+ + dbk:volumenum (dbk:volumenum) = dbk:volumenum *
+ - dbk:relation (String) 
+
+[dbk:bibliosource] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+ - dbk:otherclass (String) 
+
+[dbk:blockquote] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:attribution (dbk:attribution) = dbk:attribution
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:book] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:acknowledgements (dbk:acknowledgements) = dbk:acknowledgements *
+ + dbk:appendix (dbk:appendix) = dbk:appendix *
+ + dbk:article (dbk:article) = dbk:article *
+ + dbk:bibliography (dbk:bibliography) = dbk:bibliography *
+ + dbk:chapter (dbk:chapter) = dbk:chapter *
+ + dbk:colophon (dbk:colophon) = dbk:colophon *
+ + dbk:dedication (dbk:dedication) = dbk:dedication *
+ + dbk:glossary (dbk:glossary) = dbk:glossary *
+ + dbk:index (dbk:index) = dbk:index *
+ + dbk:part (dbk:part) = dbk:part *
+ + dbk:preface (dbk:preface) = dbk:preface *
+ + dbk:reference (dbk:reference) = dbk:reference *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:toc (dbk:toc) = dbk:toc *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:bridgehead] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:otherrenderas (String) 
+ - dbk:renderas (String) 
+
+[dbk:callout] > argeodbk:base, argeodbk:indexingInlines, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:arearefs (String) 
+
+[dbk:calloutlist] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:callout (dbk:callout) = dbk:callout *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:caption] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+ - dbk:class (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+
+[dbk:caution] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:chapter] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:refentry (dbk:refentry) = dbk:refentry *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:sect1 (dbk:sect1) = dbk:sect1 *
+ + dbk:section (dbk:section) = dbk:section *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+
+[dbk:citation] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:citebiblioid] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+ - dbk:otherclass (String) 
+
+[dbk:citerefentry] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:manvolnum (dbk:manvolnum) = dbk:manvolnum
+ + dbk:refentrytitle (dbk:refentrytitle) = dbk:refentrytitle
+
+[dbk:citetitle] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:pubwork (String) 
+
+[dbk:city] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:classname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:classsynopsis] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:classsynopsisinfo (dbk:classsynopsisinfo) = dbk:classsynopsisinfo *
+ + dbk:constructorsynopsis (dbk:constructorsynopsis) = dbk:constructorsynopsis *
+ + dbk:destructorsynopsis (dbk:destructorsynopsis) = dbk:destructorsynopsis *
+ + dbk:fieldsynopsis (dbk:fieldsynopsis) = dbk:fieldsynopsis *
+ + dbk:methodsynopsis (dbk:methodsynopsis) = dbk:methodsynopsis *
+ + dbk:ooclass (dbk:ooclass) = dbk:ooclass *
+ + dbk:ooexception (dbk:ooexception) = dbk:ooexception *
+ + dbk:oointerface (dbk:oointerface) = dbk:oointerface *
+ - dbk:class (String) 
+ - dbk:language (String) 
+
+[dbk:classsynopsisinfo] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:co (dbk:co) = dbk:co *
+ + dbk:info (dbk:info) = dbk:info *
+ + dbk:lineannotation (dbk:lineannotation) = dbk:lineannotation *
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ - dbk:continuation (String) 
+ - dbk:language (String) 
+ - dbk:linenumbering (String) 
+ - dbk:startinglinenumber (String) 
+ - xml:space (String) 
+
+[dbk:cmdsynopsis] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:arg (dbk:arg) = dbk:arg *
+ + dbk:command (dbk:command) = dbk:command *
+ + dbk:group (dbk:group) = dbk:group *
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:sbr (dbk:sbr) = dbk:sbr *
+ + dbk:synopfragment (dbk:synopfragment) = dbk:synopfragment *
+ - dbk:cmdlength (String) 
+ - dbk:label (String) 
+ - dbk:sepchar (String) 
+
+[dbk:co] > argeodbk:base
+ - dbk:label (String) 
+ - dbk:linkends (String) 
+
+[dbk:code] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:classname (dbk:classname) = dbk:classname *
+ + dbk:exceptionname (dbk:exceptionname) = dbk:exceptionname *
+ + dbk:function (dbk:function) = dbk:function *
+ + dbk:initializer (dbk:initializer) = dbk:initializer *
+ + dbk:interfacename (dbk:interfacename) = dbk:interfacename *
+ + dbk:methodname (dbk:methodname) = dbk:methodname *
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:ooclass (dbk:ooclass) = dbk:ooclass *
+ + dbk:ooexception (dbk:ooexception) = dbk:ooexception *
+ + dbk:oointerface (dbk:oointerface) = dbk:oointerface *
+ + dbk:parameter (dbk:parameter) = dbk:parameter *
+ + dbk:returnvalue (dbk:returnvalue) = dbk:returnvalue *
+ + dbk:type (dbk:type) = dbk:type *
+ + dbk:varname (dbk:varname) = dbk:varname *
+ - dbk:language (String) 
+
+[dbk:col] > nt:base
+ - dbk:align (String) 
+ - dbk:annotations (String) 
+ - dbk:arch (String) 
+ - dbk:audience (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:condition (String) 
+ - dbk:conformance (String) 
+ - dbk:dir (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:os (String) 
+ - dbk:remap (String) 
+ - dbk:revision (String) 
+ - dbk:revisionflag (String) 
+ - dbk:security (String) 
+ - dbk:span (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:userlevel (String) 
+ - dbk:valign (String) 
+ - dbk:vendor (String) 
+ - dbk:version (String) 
+ - dbk:width (String) 
+ - dbk:wordsize (String) 
+ - dbk:xreflabel (String) 
+ - xml:base (String) 
+ - xml:id (String) 
+ - xml:lang (String) 
+
+[dbk:colgroup] > nt:base
+ + dbk:col (dbk:col) = dbk:col *
+ - dbk:align (String) 
+ - dbk:annotations (String) 
+ - dbk:arch (String) 
+ - dbk:audience (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:condition (String) 
+ - dbk:conformance (String) 
+ - dbk:dir (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:os (String) 
+ - dbk:remap (String) 
+ - dbk:revision (String) 
+ - dbk:revisionflag (String) 
+ - dbk:security (String) 
+ - dbk:span (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:userlevel (String) 
+ - dbk:valign (String) 
+ - dbk:vendor (String) 
+ - dbk:version (String) 
+ - dbk:width (String) 
+ - dbk:wordsize (String) 
+ - dbk:xreflabel (String) 
+ - xml:base (String) 
+ - xml:id (String) 
+ - xml:lang (String) 
+
+[dbk:collab] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:affiliation (dbk:affiliation) = dbk:affiliation *
+ + dbk:org (dbk:org) = dbk:org *
+ + dbk:orgname (dbk:orgname) = dbk:orgname *
+ + dbk:person (dbk:person) = dbk:person *
+ + dbk:personname (dbk:personname) = dbk:personname *
+
+[dbk:colophon] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:colspec] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:colname (String) 
+ - dbk:colnum (String) 
+ - dbk:colsep (String) 
+ - dbk:colwidth (String) 
+ - dbk:rowsep (String) 
+
+[dbk:command] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:computeroutput] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:ubiquitousInlines
+ + dbk:co (dbk:co) = dbk:co *
+ + dbk:command (dbk:command) = dbk:command *
+ + dbk:computeroutput (dbk:computeroutput) = dbk:computeroutput *
+ + dbk:envar (dbk:envar) = dbk:envar *
+ + dbk:filename (dbk:filename) = dbk:filename *
+ + dbk:nonterminal (dbk:nonterminal) = dbk:nonterminal *
+ + dbk:option (dbk:option) = dbk:option *
+ + dbk:optional (dbk:optional) = dbk:optional *
+ + dbk:package (dbk:package) = dbk:package *
+ + dbk:parameter (dbk:parameter) = dbk:parameter *
+ + dbk:prompt (dbk:prompt) = dbk:prompt *
+ + dbk:property (dbk:property) = dbk:property *
+ + dbk:replaceable (dbk:replaceable) = dbk:replaceable *
+ + dbk:systemitem (dbk:systemitem) = dbk:systemitem *
+ + dbk:termdef (dbk:termdef) = dbk:termdef *
+ + dbk:userinput (dbk:userinput) = dbk:userinput *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:confdates] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:confgroup] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:confdates (dbk:confdates) = dbk:confdates *
+ + dbk:confnum (dbk:confnum) = dbk:confnum *
+ + dbk:confsponsor (dbk:confsponsor) = dbk:confsponsor *
+ + dbk:conftitle (dbk:conftitle) = dbk:conftitle *
+
+[dbk:confnum] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:confsponsor] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:conftitle] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:constant] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+
+[dbk:constraint] > argeodbk:base, argeodbk:linkingAttributes
+
+[dbk:constraintdef] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:constructorsynopsis] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:exceptionname (dbk:exceptionname) = dbk:exceptionname *
+ + dbk:methodname (dbk:methodname) = dbk:methodname
+ + dbk:methodparam (dbk:methodparam) = dbk:methodparam *
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:void (dbk:void) = dbk:void
+ - dbk:language (String) 
+
+[dbk:contractnum] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:contractsponsor] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:contrib] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:copyright] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:holder (dbk:holder) = dbk:holder *
+ + dbk:year (dbk:year) = dbk:year *
+
+[dbk:coref] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:label (String) 
+
+[dbk:country] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:cover] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:classsynopsis (dbk:classsynopsis) = dbk:classsynopsis *
+ + dbk:cmdsynopsis (dbk:cmdsynopsis) = dbk:cmdsynopsis *
+ + dbk:constraintdef (dbk:constraintdef) = dbk:constraintdef *
+ + dbk:constructorsynopsis (dbk:constructorsynopsis) = dbk:constructorsynopsis *
+ + dbk:destructorsynopsis (dbk:destructorsynopsis) = dbk:destructorsynopsis *
+ + dbk:fieldsynopsis (dbk:fieldsynopsis) = dbk:fieldsynopsis *
+ + dbk:funcsynopsis (dbk:funcsynopsis) = dbk:funcsynopsis *
+ + dbk:informalequation (dbk:informalequation) = dbk:informalequation *
+ + dbk:informalexample (dbk:informalexample) = dbk:informalexample *
+ + dbk:informalfigure (dbk:informalfigure) = dbk:informalfigure *
+ + dbk:informaltable (dbk:informaltable) = dbk:informaltable *
+ + dbk:literallayout (dbk:literallayout) = dbk:literallayout *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:methodsynopsis (dbk:methodsynopsis) = dbk:methodsynopsis *
+ + dbk:msgset (dbk:msgset) = dbk:msgset *
+ + dbk:productionset (dbk:productionset) = dbk:productionset *
+ + dbk:programlisting (dbk:programlisting) = dbk:programlisting *
+ + dbk:programlistingco (dbk:programlistingco) = dbk:programlistingco *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screen (dbk:screen) = dbk:screen *
+ + dbk:screenco (dbk:screenco) = dbk:screenco *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:synopsis (dbk:synopsis) = dbk:synopsis *
+ + dbk:task (dbk:task) = dbk:task *
+
+[dbk:database] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+
+[dbk:date] > argeodbk:base, argeodbk:linkingAttributes
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:dedication] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:destructorsynopsis] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:exceptionname (dbk:exceptionname) = dbk:exceptionname *
+ + dbk:methodname (dbk:methodname) = dbk:methodname
+ + dbk:methodparam (dbk:methodparam) = dbk:methodparam *
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:void (dbk:void) = dbk:void
+ - dbk:language (String) 
+
+[dbk:edition] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:editor] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:affiliation (dbk:affiliation) = dbk:affiliation *
+ + dbk:contrib (dbk:contrib) = dbk:contrib *
+ + dbk:email (dbk:email) = dbk:email *
+ + dbk:orgdiv (dbk:orgdiv) = dbk:orgdiv *
+ + dbk:orgname (dbk:orgname) = dbk:orgname
+ + dbk:personblurb (dbk:personblurb) = dbk:personblurb *
+ + dbk:personname (dbk:personname) = dbk:personname
+ + dbk:uri (dbk:uri) = dbk:uri *
+
+[dbk:email] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:emphasis] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:entry] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:markupInlines, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:publishingInlines, argeodbk:techDocElements, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:colname (String) 
+ - dbk:colsep (String) 
+ - dbk:morerows (String) 
+ - dbk:nameend (String) 
+ - dbk:namest (String) 
+ - dbk:rotate (String) 
+ - dbk:rowsep (String) 
+ - dbk:spanname (String) 
+ - dbk:valign (String) 
+
+[dbk:entrytbl] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:colspec (dbk:colspec) = dbk:colspec *
+ + dbk:spanspec (dbk:spanspec) = dbk:spanspec *
+ + dbk:tbody (dbk:tbody) = dbk:tbody
+ + dbk:thead (dbk:thead) = dbk:thead
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:colname (String) 
+ - dbk:cols (String) 
+ - dbk:colsep (String) 
+ - dbk:nameend (String) 
+ - dbk:namest (String) 
+ - dbk:rowsep (String) 
+ - dbk:spanname (String) 
+ - dbk:tgroupstyle (String) 
+
+[dbk:envar] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:epigraph] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:paragraphElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:attribution (dbk:attribution) = dbk:attribution
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:literallayout (dbk:literallayout) = dbk:literallayout *
+
+[dbk:equation] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:alt (dbk:alt) = dbk:alt
+ + dbk:caption (dbk:caption) = dbk:caption
+ + dbk:mathphrase (dbk:mathphrase) = dbk:mathphrase *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ - dbk:floatstyle (String) 
+ - dbk:label (String) 
+ - dbk:pgwide (String) 
+
+[dbk:errorcode] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:errorname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:errortext] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:errortype] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:example] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:caption (dbk:caption) = dbk:caption
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:floatstyle (String) 
+ - dbk:label (String) 
+ - dbk:pgwide (String) 
+ - dbk:width (String) 
+
+[dbk:exceptionname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:extendedlink] > argeodbk:base
+ + dbk:arc (dbk:arc) = dbk:arc *
+ + dbk:locator (dbk:locator) = dbk:locator *
+
+[dbk:fax] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:fieldsynopsis] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:initializer (dbk:initializer) = dbk:initializer
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:type (dbk:type) = dbk:type
+ + dbk:varname (dbk:varname) = dbk:varname
+ - dbk:language (String) 
+
+[dbk:figure] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:caption (dbk:caption) = dbk:caption
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:floatstyle (String) 
+ - dbk:label (String) 
+ - dbk:pgwide (String) 
+
+[dbk:filename] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+ - dbk:path (String) 
+
+[dbk:firstname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:firstterm] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:baseform (String) 
+
+[dbk:footnote] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:label (String) 
+
+[dbk:footnoteref] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:label (String) 
+
+[dbk:foreignphrase] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:publishingInlines
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:application (dbk:application) = dbk:application *
+ + dbk:biblioref (dbk:biblioref) = dbk:biblioref *
+ + dbk:database (dbk:database) = dbk:database *
+ + dbk:hardware (dbk:hardware) = dbk:hardware *
+ + dbk:inlinemediaobject (dbk:inlinemediaobject) = dbk:inlinemediaobject *
+ + dbk:link (dbk:link) = dbk:link *
+ + dbk:olink (dbk:olink) = dbk:olink *
+ + dbk:phrase (dbk:phrase) = dbk:phrase *
+ + dbk:productname (dbk:productname) = dbk:productname *
+ + dbk:productnumber (dbk:productnumber) = dbk:productnumber *
+ + dbk:subscript (dbk:subscript) = dbk:subscript *
+ + dbk:superscript (dbk:superscript) = dbk:superscript *
+ + dbk:trademark (dbk:trademark) = dbk:trademark *
+ + dbk:xref (dbk:xref) = dbk:xref *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:formalpara] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:para (dbk:para) = dbk:para
+
+[dbk:funcdef] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:function (dbk:function) = dbk:function *
+ + dbk:type (dbk:type) = dbk:type *
+
+[dbk:funcparams] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:funcprototype] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:funcdef (dbk:funcdef) = dbk:funcdef
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:paramdef (dbk:paramdef) = dbk:paramdef *
+ + dbk:varargs (dbk:varargs) = dbk:varargs
+ + dbk:varargs (dbk:varargs) = dbk:varargs
+ + dbk:void (dbk:void) = dbk:void
+
+[dbk:funcsynopsis] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:funcprototype (dbk:funcprototype) = dbk:funcprototype *
+ + dbk:funcsynopsisinfo (dbk:funcsynopsisinfo) = dbk:funcsynopsisinfo *
+ + dbk:info (dbk:info) = dbk:info
+ - dbk:language (String) 
+
+[dbk:funcsynopsisinfo] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:co (dbk:co) = dbk:co *
+ + dbk:info (dbk:info) = dbk:info *
+ + dbk:lineannotation (dbk:lineannotation) = dbk:lineannotation *
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ - dbk:continuation (String) 
+ - dbk:language (String) 
+ - dbk:linenumbering (String) 
+ - dbk:startinglinenumber (String) 
+ - xml:space (String) 
+
+[dbk:function] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:glossary] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bibliography (dbk:bibliography) = dbk:bibliography
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:glossdiv (dbk:glossdiv) = dbk:glossdiv *
+ + dbk:glossentry (dbk:glossentry) = dbk:glossentry *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:glossdef] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:glossseealso (dbk:glossseealso) = dbk:glossseealso *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:subject (String) 
+
+[dbk:glossdiv] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:glossentry (dbk:glossentry) = dbk:glossentry *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:glossentry] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes
+ + dbk:abbrev (dbk:abbrev) = dbk:abbrev
+ + dbk:acronym (dbk:acronym) = dbk:acronym
+ + dbk:glossdef (dbk:glossdef) = dbk:glossdef *
+ + dbk:glosssee (dbk:glosssee) = dbk:glosssee
+ + dbk:glossterm (dbk:glossterm) = dbk:glossterm
+ - dbk:sortas (String) 
+
+[dbk:glosslist] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:glossentry (dbk:glossentry) = dbk:glossentry *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:glosssee] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:otherterm (Reference) 
+
+[dbk:glossseealso] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:otherterm (Reference) 
+
+[dbk:glossterm] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:baseform (String) 
+
+[dbk:group] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:arg (dbk:arg) = dbk:arg *
+ + dbk:group (dbk:group) = dbk:group *
+ + dbk:option (dbk:option) = dbk:option *
+ + dbk:replaceable (dbk:replaceable) = dbk:replaceable *
+ + dbk:sbr (dbk:sbr) = dbk:sbr *
+ + dbk:synopfragmentref (dbk:synopfragmentref) = dbk:synopfragmentref *
+ - dbk:choice (String) 
+ - dbk:rep (String) 
+
+[dbk:guibutton] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:accel (dbk:accel) = dbk:accel *
+
+[dbk:guiicon] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:accel (dbk:accel) = dbk:accel *
+
+[dbk:guilabel] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:accel (dbk:accel) = dbk:accel *
+
+[dbk:guimenu] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:accel (dbk:accel) = dbk:accel *
+
+[dbk:guimenuitem] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:accel (dbk:accel) = dbk:accel *
+
+[dbk:guisubmenu] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:accel (dbk:accel) = dbk:accel *
+
+[dbk:hardware] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:holder] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:honorific] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:imagedata] > argeodbk:base
+ + dbk:info (dbk:info) = dbk:info
+ - dbk:align (String) 
+ - dbk:contentdepth (String) 
+ - dbk:contentwidth (String) 
+ - dbk:depth (String) 
+ - dbk:entityref (String) 
+ - dbk:fileref (String) 
+ - dbk:format (String) 
+ - dbk:scale (String) 
+ - dbk:scalefit (String) 
+ - dbk:valign (String) 
+ - dbk:width (String) 
+
+[dbk:imageobject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:imagedata (dbk:imagedata) = dbk:imagedata
+ + dbk:info (dbk:info) = dbk:info
+
+[dbk:imageobjectco] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:areaspec (dbk:areaspec) = dbk:areaspec
+ + dbk:calloutlist (dbk:calloutlist) = dbk:calloutlist *
+ + dbk:imageobject (dbk:imageobject) = dbk:imageobject *
+ + dbk:info (dbk:info) = dbk:info
+
+[dbk:important] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:index] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:indexdiv (dbk:indexdiv) = dbk:indexdiv *
+ + dbk:indexentry (dbk:indexentry) = dbk:indexentry *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+ - dbk:type (String) 
+
+[dbk:indexdiv] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:indexentry (dbk:indexentry) = dbk:indexentry *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:indexentry] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:primaryie (dbk:primaryie) = dbk:primaryie
+ + dbk:secondaryie (dbk:secondaryie) = dbk:secondaryie *
+ + dbk:seealsoie (dbk:seealsoie) = dbk:seealsoie *
+ + dbk:seeie (dbk:seeie) = dbk:seeie *
+ + dbk:tertiaryie (dbk:tertiaryie) = dbk:tertiaryie *
+
+[dbk:indexterm] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:primary (dbk:primary) = dbk:primary
+ + dbk:secondary (dbk:secondary) = dbk:secondary
+ + dbk:see (dbk:see) = dbk:see
+ + dbk:seealso (dbk:seealso) = dbk:seealso *
+ + dbk:tertiary (dbk:tertiary) = dbk:tertiary
+ - dbk:class (String) 
+ - dbk:pagenum (String) 
+ - dbk:scope (String) 
+ - dbk:significance (String) 
+ - dbk:startref (Reference) 
+ - dbk:type (String) 
+ - dbk:zone (String) 
+
+[dbk:info] > argeodbk:base
+ + dbk:abstract (dbk:abstract) = dbk:abstract *
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:artpagenums (dbk:artpagenums) = dbk:artpagenums *
+ + dbk:author (dbk:author) = dbk:author *
+ + dbk:authorgroup (dbk:authorgroup) = dbk:authorgroup *
+ + dbk:authorinitials (dbk:authorinitials) = dbk:authorinitials *
+ + dbk:bibliocoverage (dbk:bibliocoverage) = dbk:bibliocoverage *
+ + dbk:biblioid (dbk:biblioid) = dbk:biblioid *
+ + dbk:bibliomisc (dbk:bibliomisc) = dbk:bibliomisc *
+ + dbk:bibliomset (dbk:bibliomset) = dbk:bibliomset *
+ + dbk:bibliorelation (dbk:bibliorelation) = dbk:bibliorelation *
+ + dbk:biblioset (dbk:biblioset) = dbk:biblioset *
+ + dbk:bibliosource (dbk:bibliosource) = dbk:bibliosource *
+ + dbk:collab (dbk:collab) = dbk:collab *
+ + dbk:confgroup (dbk:confgroup) = dbk:confgroup *
+ + dbk:contractnum (dbk:contractnum) = dbk:contractnum *
+ + dbk:contractsponsor (dbk:contractsponsor) = dbk:contractsponsor *
+ + dbk:copyright (dbk:copyright) = dbk:copyright *
+ + dbk:cover (dbk:cover) = dbk:cover *
+ + dbk:date (dbk:date) = dbk:date *
+ + dbk:edition (dbk:edition) = dbk:edition *
+ + dbk:editor (dbk:editor) = dbk:editor *
+ + dbk:extendedlink (dbk:extendedlink) = dbk:extendedlink *
+ + dbk:issuenum (dbk:issuenum) = dbk:issuenum *
+ + dbk:itermset (dbk:itermset) = dbk:itermset *
+ + dbk:keywordset (dbk:keywordset) = dbk:keywordset *
+ + dbk:legalnotice (dbk:legalnotice) = dbk:legalnotice *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:org (dbk:org) = dbk:org *
+ + dbk:orgname (dbk:orgname) = dbk:orgname *
+ + dbk:othercredit (dbk:othercredit) = dbk:othercredit *
+ + dbk:pagenums (dbk:pagenums) = dbk:pagenums *
+ + dbk:printhistory (dbk:printhistory) = dbk:printhistory *
+ + dbk:productname (dbk:productname) = dbk:productname *
+ + dbk:productnumber (dbk:productnumber) = dbk:productnumber *
+ + dbk:pubdate (dbk:pubdate) = dbk:pubdate *
+ + dbk:publisher (dbk:publisher) = dbk:publisher *
+ + dbk:publishername (dbk:publishername) = dbk:publishername *
+ + dbk:releaseinfo (dbk:releaseinfo) = dbk:releaseinfo *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:seriesvolnums (dbk:seriesvolnums) = dbk:seriesvolnums *
+ + dbk:subjectset (dbk:subjectset) = dbk:subjectset *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:title (dbk:title) = dbk:title *
+ + dbk:titleabbrev (dbk:titleabbrev) = dbk:titleabbrev *
+ + dbk:volumenum (dbk:volumenum) = dbk:volumenum *
+
+[dbk:informalequation] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:alt (dbk:alt) = dbk:alt
+ + dbk:caption (dbk:caption) = dbk:caption
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:mathphrase (dbk:mathphrase) = dbk:mathphrase *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+
+[dbk:informalexample] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:caption (dbk:caption) = dbk:caption
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:floatstyle (String) 
+ - dbk:width (String) 
+
+[dbk:informalfigure] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:caption (dbk:caption) = dbk:caption
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:floatstyle (String) 
+ - dbk:label (String) 
+ - dbk:pgwide (String) 
+
+[dbk:informaltable] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:col (dbk:col) = dbk:col *
+ + dbk:colgroup (dbk:colgroup) = dbk:colgroup *
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:tbody (dbk:tbody) = dbk:tbody *
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ + dbk:tfoot (dbk:tfoot) = dbk:tfoot
+ + dbk:tgroup (dbk:tgroup) = dbk:tgroup *
+ + dbk:thead (dbk:thead) = dbk:thead
+ + dbk:tr (dbk:tr) = dbk:tr *
+ - dbk:border (String) 
+ - dbk:cellpadding (String) 
+ - dbk:cellspacing (String) 
+ - dbk:class (String) 
+ - dbk:colsep (String) 
+ - dbk:floatstyle (String) 
+ - dbk:frame (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:orient (String) 
+ - dbk:pgwide (String) 
+ - dbk:rowheader (String) 
+ - dbk:rowsep (String) 
+ - dbk:rules (String) 
+ - dbk:style (String) 
+ - dbk:summary (String) 
+ - dbk:tabstyle (String) 
+ - dbk:title (String) 
+ - dbk:width (String) 
+
+[dbk:initializer] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:inlineequation] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:alt (dbk:alt) = dbk:alt
+ + dbk:inlinemediaobject (dbk:inlinemediaobject) = dbk:inlinemediaobject *
+ + dbk:mathphrase (dbk:mathphrase) = dbk:mathphrase *
+
+[dbk:inlinemediaobject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:alt (dbk:alt) = dbk:alt
+ + dbk:audioobject (dbk:audioobject) = dbk:audioobject *
+ + dbk:imageobject (dbk:imageobject) = dbk:imageobject *
+ + dbk:imageobjectco (dbk:imageobjectco) = dbk:imageobjectco *
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ + dbk:videoobject (dbk:videoobject) = dbk:videoobject *
+
+[dbk:interfacename] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:issuenum] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:itemizedlist] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:listitem (dbk:listitem) = dbk:listitem *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:mark (String) 
+ - dbk:spacing (String) 
+
+[dbk:itermset] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes
+
+[dbk:jobtitle] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:keycap] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:function (String) 
+ - dbk:otherfunction (String) 
+
+[dbk:keycode] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:keycombo] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:keycap (dbk:keycap) = dbk:keycap *
+ + dbk:keycombo (dbk:keycombo) = dbk:keycombo *
+ + dbk:keysym (dbk:keysym) = dbk:keysym *
+ + dbk:mousebutton (dbk:mousebutton) = dbk:mousebutton *
+ - dbk:action (String) 
+ - dbk:otheraction (String) 
+
+[dbk:keysym] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:keyword] > argeodbk:base, argeodbk:linkingAttributes
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:keywordset] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:keyword (dbk:keyword) = dbk:keyword *
+
+[dbk:label] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:legalnotice] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:lhs] > argeodbk:base, argeodbk:linkingAttributes
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:lineage] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:lineannotation] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:link] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:endterm (Reference) 
+ - dbk:xrefstyle (String) 
+
+[dbk:listitem] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:override (String) 
+
+[dbk:literal] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:literallayout] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:co (dbk:co) = dbk:co *
+ + dbk:info (dbk:info) = dbk:info *
+ + dbk:lineannotation (dbk:lineannotation) = dbk:lineannotation *
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ - dbk:class (String) 
+ - dbk:continuation (String) 
+ - dbk:language (String) 
+ - dbk:linenumbering (String) 
+ - dbk:startinglinenumber (String) 
+ - xml:space (String) 
+
+[dbk:locator] > argeodbk:base
+ - xlink:label (String) 
+
+[dbk:manvolnum] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:markup] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:mathphrase] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:emphasis (dbk:emphasis) = dbk:emphasis *
+
+[dbk:mediaobject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:alt (dbk:alt) = dbk:alt
+ + dbk:audioobject (dbk:audioobject) = dbk:audioobject *
+ + dbk:caption (dbk:caption) = dbk:caption
+ + dbk:imageobject (dbk:imageobject) = dbk:imageobject *
+ + dbk:imageobjectco (dbk:imageobjectco) = dbk:imageobjectco *
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ + dbk:videoobject (dbk:videoobject) = dbk:videoobject *
+
+[dbk:member] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:menuchoice] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:guibutton (dbk:guibutton) = dbk:guibutton *
+ + dbk:guiicon (dbk:guiicon) = dbk:guiicon *
+ + dbk:guilabel (dbk:guilabel) = dbk:guilabel *
+ + dbk:guimenu (dbk:guimenu) = dbk:guimenu *
+ + dbk:guimenuitem (dbk:guimenuitem) = dbk:guimenuitem *
+ + dbk:guisubmenu (dbk:guisubmenu) = dbk:guisubmenu *
+ + dbk:shortcut (dbk:shortcut) = dbk:shortcut
+
+[dbk:methodname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:methodparam] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:funcparams (dbk:funcparams) = dbk:funcparams
+ + dbk:initializer (dbk:initializer) = dbk:initializer
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:parameter (dbk:parameter) = dbk:parameter
+ + dbk:type (dbk:type) = dbk:type *
+ - dbk:choice (String) 
+ - dbk:rep (String) 
+
+[dbk:methodsynopsis] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:exceptionname (dbk:exceptionname) = dbk:exceptionname *
+ + dbk:methodname (dbk:methodname) = dbk:methodname
+ + dbk:methodparam (dbk:methodparam) = dbk:methodparam *
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:type (dbk:type) = dbk:type
+ + dbk:void (dbk:void) = dbk:void
+ - dbk:language (String) 
+
+[dbk:modifier] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - xml:space (String) 
+
+[dbk:mousebutton] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:msg] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:msgmain (dbk:msgmain) = dbk:msgmain
+ + dbk:msgrel (dbk:msgrel) = dbk:msgrel *
+ + dbk:msgsub (dbk:msgsub) = dbk:msgsub *
+
+[dbk:msgaud] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:msgentry] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:msg (dbk:msg) = dbk:msg *
+ + dbk:msgexplan (dbk:msgexplan) = dbk:msgexplan *
+ + dbk:msginfo (dbk:msginfo) = dbk:msginfo
+
+[dbk:msgexplan] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:msginfo] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:msgaud (dbk:msgaud) = dbk:msgaud *
+ + dbk:msglevel (dbk:msglevel) = dbk:msglevel *
+ + dbk:msgorig (dbk:msgorig) = dbk:msgorig *
+
+[dbk:msglevel] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:msgmain] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:msgtext (dbk:msgtext) = dbk:msgtext
+
+[dbk:msgorig] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:msgrel] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:msgtext (dbk:msgtext) = dbk:msgtext
+
+[dbk:msgset] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:msgentry (dbk:msgentry) = dbk:msgentry *
+ + dbk:simplemsgentry (dbk:simplemsgentry) = dbk:simplemsgentry *
+
+[dbk:msgsub] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:msgtext (dbk:msgtext) = dbk:msgtext
+
+[dbk:msgtext] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:nonterminal] > argeodbk:base, argeodbk:linkingAttributes
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+ - dbk:def (String) 
+
+[dbk:note] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:olink] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:localinfo (String) 
+ - dbk:targetdoc (String) 
+ - dbk:targetptr (String) 
+ - dbk:type (String) 
+ - dbk:xrefstyle (String) 
+
+[dbk:ooclass] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:classname (dbk:classname) = dbk:classname
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:package (dbk:package) = dbk:package *
+
+[dbk:ooexception] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:exceptionname (dbk:exceptionname) = dbk:exceptionname
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:package (dbk:package) = dbk:package *
+
+[dbk:oointerface] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:interfacename (dbk:interfacename) = dbk:interfacename
+ + dbk:modifier (dbk:modifier) = dbk:modifier *
+ + dbk:package (dbk:package) = dbk:package *
+
+[dbk:option] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:optional] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:orderedlist] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:listitem (dbk:listitem) = dbk:listitem *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:continuation (String) 
+ - dbk:inheritnum (String) 
+ - dbk:numeration (String) 
+ - dbk:spacing (String) 
+ - dbk:startingnumber (String) 
+
+[dbk:org] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:affiliation (dbk:affiliation) = dbk:affiliation *
+ + dbk:email (dbk:email) = dbk:email *
+ + dbk:orgdiv (dbk:orgdiv) = dbk:orgdiv *
+ + dbk:orgname (dbk:orgname) = dbk:orgname
+ + dbk:uri (dbk:uri) = dbk:uri *
+
+[dbk:orgdiv] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:orgname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+ - dbk:otherclass (String) 
+
+[dbk:otheraddr] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:othercredit] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:affiliation (dbk:affiliation) = dbk:affiliation *
+ + dbk:contrib (dbk:contrib) = dbk:contrib *
+ + dbk:email (dbk:email) = dbk:email *
+ + dbk:orgdiv (dbk:orgdiv) = dbk:orgdiv *
+ + dbk:orgname (dbk:orgname) = dbk:orgname
+ + dbk:personblurb (dbk:personblurb) = dbk:personblurb *
+ + dbk:personname (dbk:personname) = dbk:personname
+ + dbk:uri (dbk:uri) = dbk:uri *
+ - dbk:class (String) 
+ - dbk:otherclass (String) 
+
+[dbk:othername] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:package] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:pagenums] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:para] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:markupInlines, argeodbk:publishingElements, argeodbk:publishingInlines, argeodbk:techDocElements, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:info (dbk:info) = dbk:info *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:paramdef] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:funcparams (dbk:funcparams) = dbk:funcparams *
+ + dbk:initializer (dbk:initializer) = dbk:initializer *
+ + dbk:parameter (dbk:parameter) = dbk:parameter *
+ + dbk:type (dbk:type) = dbk:type *
+ - dbk:choice (String) 
+
+[dbk:parameter] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+
+[dbk:part] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:acknowledgements (dbk:acknowledgements) = dbk:acknowledgements *
+ + dbk:appendix (dbk:appendix) = dbk:appendix *
+ + dbk:article (dbk:article) = dbk:article *
+ + dbk:bibliography (dbk:bibliography) = dbk:bibliography *
+ + dbk:chapter (dbk:chapter) = dbk:chapter *
+ + dbk:colophon (dbk:colophon) = dbk:colophon *
+ + dbk:dedication (dbk:dedication) = dbk:dedication *
+ + dbk:glossary (dbk:glossary) = dbk:glossary *
+ + dbk:index (dbk:index) = dbk:index *
+ + dbk:partintro (dbk:partintro) = dbk:partintro
+ + dbk:preface (dbk:preface) = dbk:preface *
+ + dbk:refentry (dbk:refentry) = dbk:refentry *
+ + dbk:reference (dbk:reference) = dbk:reference *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:toc (dbk:toc) = dbk:toc *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:partintro] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:refentry (dbk:refentry) = dbk:refentry *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:sect1 (dbk:sect1) = dbk:sect1 *
+ + dbk:section (dbk:section) = dbk:section *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:person] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:affiliation (dbk:affiliation) = dbk:affiliation *
+ + dbk:email (dbk:email) = dbk:email *
+ + dbk:personblurb (dbk:personblurb) = dbk:personblurb *
+ + dbk:personname (dbk:personname) = dbk:personname
+ + dbk:uri (dbk:uri) = dbk:uri *
+
+[dbk:personblurb] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:paragraphElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+
+[dbk:personname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:firstname (dbk:firstname) = dbk:firstname *
+ + dbk:honorific (dbk:honorific) = dbk:honorific *
+ + dbk:lineage (dbk:lineage) = dbk:lineage *
+ + dbk:othername (dbk:othername) = dbk:othername *
+ + dbk:surname (dbk:surname) = dbk:surname *
+
+[dbk:phone] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:phrase] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:pob] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:postcode] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:preface] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:refentry (dbk:refentry) = dbk:refentry *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:sect1 (dbk:sect1) = dbk:sect1 *
+ + dbk:section (dbk:section) = dbk:section *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+
+[dbk:primary] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:sortas (String) 
+
+[dbk:primaryie] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:linkends (String) 
+
+[dbk:printhistory] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:paragraphElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+
+[dbk:procedure] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:step (dbk:step) = dbk:step *
+
+[dbk:production] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:constraint (dbk:constraint) = dbk:constraint *
+ + dbk:lhs (dbk:lhs) = dbk:lhs
+ + dbk:rhs (dbk:rhs) = dbk:rhs
+
+[dbk:productionrecap] > argeodbk:base, argeodbk:linkingAttributes
+
+[dbk:productionset] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:production (dbk:production) = dbk:production *
+ + dbk:productionrecap (dbk:productionrecap) = dbk:productionrecap *
+
+[dbk:productname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+
+[dbk:productnumber] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:programlisting] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:co (dbk:co) = dbk:co *
+ + dbk:info (dbk:info) = dbk:info *
+ + dbk:lineannotation (dbk:lineannotation) = dbk:lineannotation *
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ - dbk:continuation (String) 
+ - dbk:language (String) 
+ - dbk:linenumbering (String) 
+ - dbk:startinglinenumber (String) 
+ - dbk:width (String) 
+ - xml:space (String) 
+
+[dbk:programlistingco] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:areaspec (dbk:areaspec) = dbk:areaspec
+ + dbk:calloutlist (dbk:calloutlist) = dbk:calloutlist *
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:programlisting (dbk:programlisting) = dbk:programlisting
+
+[dbk:prompt] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:co (dbk:co) = dbk:co *
+
+[dbk:property] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:pubdate] > argeodbk:base, argeodbk:linkingAttributes
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:publisher] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:address (dbk:address) = dbk:address *
+ + dbk:publishername (dbk:publishername) = dbk:publishername
+
+[dbk:publishername] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:qandadiv] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:qandadiv (dbk:qandadiv) = dbk:qandadiv *
+ + dbk:qandaentry (dbk:qandaentry) = dbk:qandaentry *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:qandaentry] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:answer (dbk:answer) = dbk:answer *
+ + dbk:question (dbk:question) = dbk:question
+
+[dbk:qandaset] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:qandadiv (dbk:qandadiv) = dbk:qandadiv *
+ + dbk:qandaentry (dbk:qandaentry) = dbk:qandaentry *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:defaultlabel (String) 
+
+[dbk:question] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:label (dbk:label) = dbk:label
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:quote] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:refclass] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:application (dbk:application) = dbk:application *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:refdescriptor] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:refentry] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:refmeta (dbk:refmeta) = dbk:refmeta
+ + dbk:refnamediv (dbk:refnamediv) = dbk:refnamediv *
+ + dbk:refsect1 (dbk:refsect1) = dbk:refsect1 *
+ + dbk:refsection (dbk:refsection) = dbk:refsection *
+ + dbk:refsynopsisdiv (dbk:refsynopsisdiv) = dbk:refsynopsisdiv
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:refentrytitle] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:reference] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:partintro (dbk:partintro) = dbk:partintro
+ + dbk:refentry (dbk:refentry) = dbk:refentry *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:refmeta] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes
+ + dbk:manvolnum (dbk:manvolnum) = dbk:manvolnum
+ + dbk:refentrytitle (dbk:refentrytitle) = dbk:refentrytitle
+ + dbk:refmiscinfo (dbk:refmiscinfo) = dbk:refmiscinfo *
+
+[dbk:refmiscinfo] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+ - dbk:otherclass (String) 
+
+[dbk:refname] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:refnamediv] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:refclass (dbk:refclass) = dbk:refclass *
+ + dbk:refdescriptor (dbk:refdescriptor) = dbk:refdescriptor
+ + dbk:refname (dbk:refname) = dbk:refname *
+ + dbk:refpurpose (dbk:refpurpose) = dbk:refpurpose
+
+[dbk:refpurpose] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:refsect1] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:refsect2 (dbk:refsect2) = dbk:refsect2 *
+ + dbk:refsect2 (dbk:refsect2) = dbk:refsect2 *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:refsect2] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:refsect3 (dbk:refsect3) = dbk:refsect3 *
+ + dbk:refsect3 (dbk:refsect3) = dbk:refsect3 *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:refsect3] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:refsection] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:refsection (dbk:refsection) = dbk:refsection *
+ + dbk:refsection (dbk:refsection) = dbk:refsection *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:refsynopsisdiv] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:refsect2 (dbk:refsect2) = dbk:refsect2 *
+ + dbk:refsection (dbk:refsection) = dbk:refsection *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+
+[dbk:releaseinfo] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:remark] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:replaceable] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:co (dbk:co) = dbk:co *
+ - dbk:class (String) 
+
+[dbk:returnvalue] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:revdescription] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:revhistory] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:revision (dbk:revision) = dbk:revision *
+
+[dbk:revision] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:author (dbk:author) = dbk:author *
+ + dbk:authorinitials (dbk:authorinitials) = dbk:authorinitials *
+ + dbk:date (dbk:date) = dbk:date
+ + dbk:revdescription (dbk:revdescription) = dbk:revdescription
+ + dbk:revnumber (dbk:revnumber) = dbk:revnumber
+ + dbk:revremark (dbk:revremark) = dbk:revremark
+
+[dbk:revnumber] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:revremark] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:rhs] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:lineannotation (dbk:lineannotation) = dbk:lineannotation *
+ + dbk:nonterminal (dbk:nonterminal) = dbk:nonterminal *
+ + dbk:sbr (dbk:sbr) = dbk:sbr *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:row] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:entry (dbk:entry) = dbk:entry *
+ + dbk:entrytbl (dbk:entrytbl) = dbk:entrytbl *
+ - dbk:rowsep (String) 
+ - dbk:valign (String) 
+
+[dbk:sbr] > argeodbk:base
+
+[dbk:screen] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:co (dbk:co) = dbk:co *
+ + dbk:info (dbk:info) = dbk:info *
+ + dbk:lineannotation (dbk:lineannotation) = dbk:lineannotation *
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ - dbk:continuation (String) 
+ - dbk:language (String) 
+ - dbk:linenumbering (String) 
+ - dbk:startinglinenumber (String) 
+ - dbk:width (String) 
+ - xml:space (String) 
+
+[dbk:screenco] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:areaspec (dbk:areaspec) = dbk:areaspec
+ + dbk:calloutlist (dbk:calloutlist) = dbk:calloutlist *
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:screen (dbk:screen) = dbk:screen
+
+[dbk:screenshot] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+
+[dbk:secondary] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:sortas (String) 
+
+[dbk:secondaryie] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:linkends (String) 
+
+[dbk:sect1] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:sect2 (dbk:sect2) = dbk:sect2 *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+
+[dbk:sect2] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:sect3 (dbk:sect3) = dbk:sect3 *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+
+[dbk:sect3] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:sect4 (dbk:sect4) = dbk:sect4 *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+
+[dbk:sect4] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:sect5 (dbk:sect5) = dbk:sect5 *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+
+[dbk:sect5] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+
+[dbk:section] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:refentry (dbk:refentry) = dbk:refentry *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:section (dbk:section) = dbk:section *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+ + dbk:simplesect (dbk:simplesect) = dbk:simplesect *
+
+[dbk:see] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:seealso] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:seealsoie] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:linkends (String) 
+
+[dbk:seeie] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:seg] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:seglistitem] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:seg (dbk:seg) = dbk:seg *
+
+[dbk:segmentedlist] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:seglistitem (dbk:seglistitem) = dbk:seglistitem *
+ + dbk:segtitle (dbk:segtitle) = dbk:segtitle *
+
+[dbk:segtitle] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:seriesvolnums] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:set] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:book (dbk:book) = dbk:book *
+ + dbk:set (dbk:set) = dbk:set *
+ + dbk:setindex (dbk:setindex) = dbk:setindex
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:toc (dbk:toc) = dbk:toc
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:setindex] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:indexdiv (dbk:indexdiv) = dbk:indexdiv *
+ + dbk:indexentry (dbk:indexentry) = dbk:indexentry *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+ - dbk:type (String) 
+
+[dbk:shortaffil] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:shortcut] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:keycap (dbk:keycap) = dbk:keycap *
+ + dbk:keycombo (dbk:keycombo) = dbk:keycombo *
+ + dbk:keysym (dbk:keysym) = dbk:keysym *
+ + dbk:mousebutton (dbk:mousebutton) = dbk:mousebutton *
+ - dbk:action (String) 
+ - dbk:otheraction (String) 
+
+[dbk:sidebar] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:simpara] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:info (dbk:info) = dbk:info *
+
+[dbk:simplelist] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:member (dbk:member) = dbk:member *
+ - dbk:columns (String) 
+ - dbk:type (String) 
+
+[dbk:simplemsgentry] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:msgexplan (dbk:msgexplan) = dbk:msgexplan *
+ + dbk:msgtext (dbk:msgtext) = dbk:msgtext
+ - dbk:msgaud (String) 
+ - dbk:msglevel (String) 
+ - dbk:msgorig (String) 
+
+[dbk:simplesect] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:spanspec] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:colsep (String) 
+ - dbk:nameend (String) 
+ - dbk:namest (String) 
+ - dbk:rowsep (String) 
+ - dbk:spanname (String) 
+
+[dbk:state] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:step] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:stepalternatives (dbk:stepalternatives) = dbk:stepalternatives
+ + dbk:substeps (dbk:substeps) = dbk:substeps
+ - dbk:performance (String) 
+
+[dbk:stepalternatives] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:step (dbk:step) = dbk:step *
+ - dbk:performance (String) 
+
+[dbk:street] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:subject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:subjectterm (dbk:subjectterm) = dbk:subjectterm *
+ - dbk:weight (String) 
+
+[dbk:subjectset] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:subject (dbk:subject) = dbk:subject *
+ - dbk:scheme (String) 
+
+[dbk:subjectterm] > argeodbk:base, argeodbk:linkingAttributes
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:subscript] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:substeps] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:step (dbk:step) = dbk:step *
+ - dbk:performance (String) 
+
+[dbk:subtitle] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:superscript] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:surname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:symbol] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+
+[dbk:synopfragment] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:arg (dbk:arg) = dbk:arg *
+ + dbk:group (dbk:group) = dbk:group *
+
+[dbk:synopfragmentref] > argeodbk:base, argeodbk:linkingAttributes
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:synopsis] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:co (dbk:co) = dbk:co *
+ + dbk:info (dbk:info) = dbk:info *
+ + dbk:lineannotation (dbk:lineannotation) = dbk:lineannotation *
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ - dbk:continuation (String) 
+ - dbk:label (String) 
+ - dbk:language (String) 
+ - dbk:linenumbering (String) 
+ - dbk:startinglinenumber (String) 
+ - xml:space (String) 
+
+[dbk:systemitem] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ + dbk:co (dbk:co) = dbk:co *
+ - dbk:class (String) 
+
+[dbk:table] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:caption (dbk:caption) = dbk:caption
+ + dbk:col (dbk:col) = dbk:col *
+ + dbk:colgroup (dbk:colgroup) = dbk:colgroup *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:tbody (dbk:tbody) = dbk:tbody *
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ + dbk:tfoot (dbk:tfoot) = dbk:tfoot
+ + dbk:tgroup (dbk:tgroup) = dbk:tgroup *
+ + dbk:thead (dbk:thead) = dbk:thead
+ + dbk:tr (dbk:tr) = dbk:tr *
+ - dbk:border (String) 
+ - dbk:cellpadding (String) 
+ - dbk:cellspacing (String) 
+ - dbk:class (String) 
+ - dbk:colsep (String) 
+ - dbk:floatstyle (String) 
+ - dbk:frame (String) 
+ - dbk:label (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:orient (String) 
+ - dbk:pgwide (String) 
+ - dbk:rowheader (String) 
+ - dbk:rowsep (String) 
+ - dbk:rules (String) 
+ - dbk:shortentry (String) 
+ - dbk:style (String) 
+ - dbk:summary (String) 
+ - dbk:tabstyle (String) 
+ - dbk:title (String) 
+ - dbk:tocentry (String) 
+ - dbk:width (String) 
+
+[dbk:tag] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+ - dbk:namespace (String) 
+
+[dbk:task] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:example (dbk:example) = dbk:example *
+ + dbk:procedure (dbk:procedure) = dbk:procedure
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:taskprerequisites (dbk:taskprerequisites) = dbk:taskprerequisites
+ + dbk:taskrelated (dbk:taskrelated) = dbk:taskrelated
+ + dbk:tasksummary (dbk:tasksummary) = dbk:tasksummary
+
+[dbk:taskprerequisites] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:taskrelated] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:tasksummary] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:tbody] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:row (dbk:row) = dbk:row *
+ + dbk:tr (dbk:tr) = dbk:tr *
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:valign (String) 
+
+[dbk:td] > argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:listElements, argeodbk:markupInlines, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:publishingInlines, argeodbk:techDocElements, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:abbr (String) 
+ - dbk:align (String) 
+ - dbk:annotations (String) 
+ - dbk:arch (String) 
+ - dbk:audience (String) 
+ - dbk:axis (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:colspan (String) 
+ - dbk:condition (String) 
+ - dbk:conformance (String) 
+ - dbk:dir (String) 
+ - dbk:headers (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:os (String) 
+ - dbk:remap (String) 
+ - dbk:revision (String) 
+ - dbk:revisionflag (String) 
+ - dbk:rowspan (String) 
+ - dbk:scope (String) 
+ - dbk:security (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:userlevel (String) 
+ - dbk:valign (String) 
+ - dbk:vendor (String) 
+ - dbk:version (String) 
+ - dbk:wordsize (String) 
+ - dbk:xreflabel (String) 
+ - xml:base (String) 
+ - xml:id (String) 
+ - xml:lang (String) 
+
+[dbk:term] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:termdef] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:baseform (String) 
+ - dbk:sortas (String) 
+
+[dbk:tertiary] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:sortas (String) 
+
+[dbk:tertiaryie] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:linkends (String) 
+
+[dbk:textdata] > argeodbk:base
+ + dbk:info (dbk:info) = dbk:info
+ - dbk:encoding (String) 
+ - dbk:entityref (String) 
+ - dbk:fileref (String) 
+ - dbk:format (String) 
+
+[dbk:textobject] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:phrase (dbk:phrase) = dbk:phrase
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:textdata (dbk:textdata) = dbk:textdata
+
+[dbk:tfoot] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:colspec (dbk:colspec) = dbk:colspec *
+ + dbk:row (dbk:row) = dbk:row *
+ + dbk:tr (dbk:tr) = dbk:tr *
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:valign (String) 
+
+[dbk:tgroup] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:colspec (dbk:colspec) = dbk:colspec *
+ + dbk:spanspec (dbk:spanspec) = dbk:spanspec *
+ + dbk:tbody (dbk:tbody) = dbk:tbody
+ + dbk:tfoot (dbk:tfoot) = dbk:tfoot
+ + dbk:thead (dbk:thead) = dbk:thead
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:cols (String) 
+ - dbk:colsep (String) 
+ - dbk:rowsep (String) 
+ - dbk:tgroupstyle (String) 
+
+[dbk:th] > argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:listElements, argeodbk:markupInlines, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:publishingInlines, argeodbk:techDocElements, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ - dbk:abbr (String) 
+ - dbk:align (String) 
+ - dbk:annotations (String) 
+ - dbk:arch (String) 
+ - dbk:audience (String) 
+ - dbk:axis (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:colspan (String) 
+ - dbk:condition (String) 
+ - dbk:conformance (String) 
+ - dbk:dir (String) 
+ - dbk:headers (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:os (String) 
+ - dbk:remap (String) 
+ - dbk:revision (String) 
+ - dbk:revisionflag (String) 
+ - dbk:rowspan (String) 
+ - dbk:scope (String) 
+ - dbk:security (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:userlevel (String) 
+ - dbk:valign (String) 
+ - dbk:vendor (String) 
+ - dbk:version (String) 
+ - dbk:wordsize (String) 
+ - dbk:xreflabel (String) 
+ - xml:base (String) 
+ - xml:id (String) 
+ - xml:lang (String) 
+
+[dbk:thead] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:colspec (dbk:colspec) = dbk:colspec *
+ + dbk:row (dbk:row) = dbk:row *
+ + dbk:tr (dbk:tr) = dbk:tr *
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:valign (String) 
+
+[dbk:tip] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:title] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:titleabbrev] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:toc] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:tocdiv (dbk:tocdiv) = dbk:tocdiv *
+ + dbk:tocentry (dbk:tocentry) = dbk:tocentry *
+
+[dbk:tocdiv] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:tocdiv (dbk:tocdiv) = dbk:tocdiv *
+ + dbk:tocentry (dbk:tocentry) = dbk:tocentry *
+ - dbk:pagenum (String) 
+
+[dbk:tocentry] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:pagenum (String) 
+
+[dbk:token] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:tr] > nt:base
+ + dbk:td (dbk:td) = dbk:td *
+ + dbk:th (dbk:th) = dbk:th *
+ - dbk:align (String) 
+ - dbk:annotations (String) 
+ - dbk:arch (String) 
+ - dbk:audience (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:condition (String) 
+ - dbk:conformance (String) 
+ - dbk:dir (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:os (String) 
+ - dbk:remap (String) 
+ - dbk:revision (String) 
+ - dbk:revisionflag (String) 
+ - dbk:security (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:userlevel (String) 
+ - dbk:valign (String) 
+ - dbk:vendor (String) 
+ - dbk:version (String) 
+ - dbk:wordsize (String) 
+ - dbk:xreflabel (String) 
+ - xml:base (String) 
+ - xml:id (String) 
+ - xml:lang (String) 
+
+[dbk:trademark] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+
+[dbk:type] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:uri] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:type (String) 
+
+[dbk:userinput] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:ubiquitousInlines
+ + dbk:accel (dbk:accel) = dbk:accel *
+ + dbk:co (dbk:co) = dbk:co *
+ + dbk:command (dbk:command) = dbk:command *
+ + dbk:computeroutput (dbk:computeroutput) = dbk:computeroutput *
+ + dbk:envar (dbk:envar) = dbk:envar *
+ + dbk:filename (dbk:filename) = dbk:filename *
+ + dbk:guibutton (dbk:guibutton) = dbk:guibutton *
+ + dbk:guiicon (dbk:guiicon) = dbk:guiicon *
+ + dbk:guilabel (dbk:guilabel) = dbk:guilabel *
+ + dbk:guimenu (dbk:guimenu) = dbk:guimenu *
+ + dbk:guimenuitem (dbk:guimenuitem) = dbk:guimenuitem *
+ + dbk:guisubmenu (dbk:guisubmenu) = dbk:guisubmenu *
+ + dbk:keycap (dbk:keycap) = dbk:keycap *
+ + dbk:keycode (dbk:keycode) = dbk:keycode *
+ + dbk:keycombo (dbk:keycombo) = dbk:keycombo *
+ + dbk:keysym (dbk:keysym) = dbk:keysym *
+ + dbk:menuchoice (dbk:menuchoice) = dbk:menuchoice *
+ + dbk:mousebutton (dbk:mousebutton) = dbk:mousebutton *
+ + dbk:nonterminal (dbk:nonterminal) = dbk:nonterminal *
+ + dbk:option (dbk:option) = dbk:option *
+ + dbk:optional (dbk:optional) = dbk:optional *
+ + dbk:package (dbk:package) = dbk:package *
+ + dbk:parameter (dbk:parameter) = dbk:parameter *
+ + dbk:prompt (dbk:prompt) = dbk:prompt *
+ + dbk:property (dbk:property) = dbk:property *
+ + dbk:replaceable (dbk:replaceable) = dbk:replaceable *
+ + dbk:shortcut (dbk:shortcut) = dbk:shortcut *
+ + dbk:systemitem (dbk:systemitem) = dbk:systemitem *
+ + dbk:termdef (dbk:termdef) = dbk:termdef *
+ + dbk:userinput (dbk:userinput) = dbk:userinput *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:varargs] > argeodbk:base, argeodbk:linkingAttributes
+
+[dbk:variablelist] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+ + dbk:varlistentry (dbk:varlistentry) = dbk:varlistentry *
+ - dbk:spacing (String) 
+ - dbk:termlength (String) 
+
+[dbk:varlistentry] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:listitem (dbk:listitem) = dbk:listitem
+ + dbk:term (dbk:term) = dbk:term *
+
+[dbk:varname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:videodata] > argeodbk:base
+ + dbk:info (dbk:info) = dbk:info
+ - dbk:align (String) 
+ - dbk:contentdepth (String) 
+ - dbk:contentwidth (String) 
+ - dbk:depth (String) 
+ - dbk:entityref (String) 
+ - dbk:fileref (String) 
+ - dbk:format (String) 
+ - dbk:scale (String) 
+ - dbk:scalefit (String) 
+ - dbk:valign (String) 
+ - dbk:width (String) 
+
+[dbk:videoobject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:videodata (dbk:videodata) = dbk:videodata
+
+[dbk:void] > argeodbk:base, argeodbk:linkingAttributes
+
+[dbk:volumenum] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:warning] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:bridgehead (dbk:bridgehead) = dbk:bridgehead *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:revhistory (dbk:revhistory) = dbk:revhistory *
+ + dbk:screenshot (dbk:screenshot) = dbk:screenshot *
+
+[dbk:wordasword] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:xmltext] > nt:base
+ - jcr:xmlcharacters (String) 
+
+[dbk:xref] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:endterm (Reference) 
+ - dbk:xrefstyle (String) 
+
+[dbk:year] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[xs:anyType] > nt:base
+ + * (nt:base) 
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+ - * (undefined) 
+
+
diff --git a/org.argeo.jcr/src/org/argeo/jcr/docbook/docbook.cnd b/org.argeo.jcr/src/org/argeo/jcr/docbook/docbook.cnd
new file mode 100644 (file)
index 0000000..74ec3cc
--- /dev/null
@@ -0,0 +1,532 @@
+<dbk = 'http://docbook.org/ns/docbook'>
+<argeodbk = 'http://www.argeo.org/ns/argeodbk'>
+<xlink = 'http://www.w3.org/1999/xlink'>
+
+[argeodbk:titled]
+mixin
+ + dbk:info (dbk:info) = dbk:info *
+ + dbk:title (dbk:title) = dbk:title *
+
+[argeodbk:linkingAttributes]
+mixin
+ - dbk:linkend (String)
+ - xlink:actuate (String)
+ - xlink:arcrole (String)
+ - xlink:href (String)
+ - xlink:role (String)
+ - xlink:show (String)
+ - xlink:title (String)
+ - xlink:type (String)
+
+[argeodbk:freeText]
+mixin
+ + dbk:phrase (dbk:phrase) = dbk:phrase *
+ + dbk:replaceable (dbk:replaceable) = dbk:replaceable *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[argeodbk:markupInlines]
+mixin
+
+[argeodbk:listElements]
+mixin
+ + dbk:itemizedlist (dbk:itemizedlist) = dbk:itemizedlist *
+ + dbk:orderedlist (dbk:orderedlist) = dbk:orderedlist *
+ + dbk:simplelist (dbk:simplelist) = dbk:simplelist *
+
+[argeodbk:paragraphElements]
+mixin
+ + dbk:para (dbk:para) = dbk:para *
+
+[argeodbk:indexingInlines]
+mixin
+
+[argeodbk:techDocElements]
+mixin
+ + dbk:table (dbk:table) = dbk:table *
+
+[argeodbk:techDocInlines]
+mixin
+
+[argeodbk:publishingElements]
+mixin
+
+[argeodbk:ubiquitousInlines]
+mixin
+ + dbk:alt (dbk:alt) = dbk:alt *
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:biblioref (dbk:biblioref) = dbk:biblioref *
+ + dbk:inlinemediaobject (dbk:inlinemediaobject) = dbk:inlinemediaobject *
+ + dbk:link (dbk:link) = dbk:link *
+ + dbk:olink (dbk:olink) = dbk:olink *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:subscript (dbk:subscript) = dbk:subscript *
+ + dbk:superscript (dbk:superscript) = dbk:superscript *
+ + dbk:xref (dbk:xref) = dbk:xref *
+
+[argeodbk:abstractSection]
+mixin
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String)
+ - dbk:status (String)
+
+[argeodbk:bibliographyInlines]
+mixin
+ + dbk:author (dbk:author) = dbk:author *
+ + dbk:editor (dbk:editor) = dbk:editor *
+ + dbk:orgname (dbk:orgname) = dbk:orgname *
+ + dbk:personname (dbk:personname) = dbk:personname *
+
+[argeodbk:publishingInlines]
+mixin
+ + dbk:emphasis (dbk:emphasis) = dbk:emphasis *
+
+[argeodbk:base]
+abstract
+orderable
+ - dbk:annotations (String)
+ - dbk:arch (String)
+ - dbk:audience (String)
+ - dbk:condition (String)
+ - dbk:conformance (String)
+ - dbk:dir (String)
+ - dbk:os (String)
+ - dbk:remap (String)
+ - dbk:revision (String)
+ - dbk:revisionflag (String)
+ - dbk:role (String)
+ - dbk:security (String)
+ - dbk:userlevel (String)
+ - dbk:vendor (String)
+ - dbk:version (String)
+ - dbk:wordsize (String)
+ - dbk:xreflabel (String)
+// - {http://www.w3.org/XML/1998/namespace}base (String)
+// - {http://www.w3.org/XML/1998/namespace}id (String)
+// - {http://www.w3.org/XML/1998/namespace}lang (String)
+
+[dbk:alt] > argeodbk:base
+ + dbk:inlinemediaobject (dbk:inlinemediaobject) = dbk:inlinemediaobject *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+
+[dbk:anchor] > argeodbk:base
+
+[dbk:annotation] > argeodbk:base, argeodbk:indexingInlines, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ - dbk:annotates (String) 
+
+[dbk:article] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:section (dbk:section) = dbk:section *
+ - dbk:class (String) 
+
+[dbk:audiodata] > argeodbk:base
+ + dbk:info (dbk:info) = dbk:info
+ - dbk:entityref (String) 
+ - dbk:fileref (String) 
+ - dbk:format (String) 
+
+[dbk:audioobject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:audiodata (dbk:audiodata) = dbk:audiodata
+ + dbk:info (dbk:info) = dbk:info
+
+[dbk:author] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:orgdiv (dbk:orgdiv) = dbk:orgdiv *
+ + dbk:orgname (dbk:orgname) = dbk:orgname
+ + dbk:personblurb (dbk:personblurb) = dbk:personblurb *
+ + dbk:personname (dbk:personname) = dbk:personname
+
+[dbk:biblioref] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:begin (String) 
+ - dbk:end (String) 
+ - dbk:endterm (Reference) 
+ - dbk:units (String) 
+ - dbk:xrefstyle (String) 
+
+[dbk:book] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:article (dbk:article) = dbk:article *
+ + dbk:chapter (dbk:chapter) = dbk:chapter *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:caption] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ + jcr:xmltext (dbk:xmltext) = dbk:xmltext *
+ - dbk:class (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+
+[dbk:chapter] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:section (dbk:section) = dbk:section *
+
+[dbk:colspec] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:colname (String) 
+ - dbk:colnum (String) 
+ - dbk:colsep (String) 
+ - dbk:colwidth (String) 
+ - dbk:rowsep (String) 
+
+[dbk:editor] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:orgdiv (dbk:orgdiv) = dbk:orgdiv *
+ + dbk:orgname (dbk:orgname) = dbk:orgname
+ + dbk:personblurb (dbk:personblurb) = dbk:personblurb *
+ + dbk:personname (dbk:personname) = dbk:personname
+
+[dbk:emphasis] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:entry] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:markupInlines, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:publishingInlines, argeodbk:techDocElements, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:colname (String) 
+ - dbk:colsep (String) 
+ - dbk:morerows (String) 
+ - dbk:nameend (String) 
+ - dbk:namest (String) 
+ - dbk:rotate (String) 
+ - dbk:rowsep (String) 
+ - dbk:spanname (String) 
+ - dbk:valign (String) 
+
+[dbk:entrytbl] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:colspec (dbk:colspec) = dbk:colspec *
+ + dbk:spanspec (dbk:spanspec) = dbk:spanspec *
+ + dbk:tbody (dbk:tbody) = dbk:tbody
+ + dbk:thead (dbk:thead) = dbk:thead
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:colname (String) 
+ - dbk:cols (String) 
+ - dbk:colsep (String) 
+ - dbk:nameend (String) 
+ - dbk:namest (String) 
+ - dbk:rowsep (String) 
+ - dbk:spanname (String) 
+ - dbk:tgroupstyle (String) 
+
+[dbk:imagedata] > argeodbk:base
+ + dbk:info (dbk:info) = dbk:info
+ - dbk:align (String) 
+ - dbk:contentdepth (String) 
+ - dbk:contentwidth (String) 
+ - dbk:depth (String) 
+ - dbk:entityref (String) 
+ - dbk:fileref (String) 
+ - dbk:format (String) 
+ - dbk:scale (String) 
+ - dbk:scalefit (String) 
+ - dbk:valign (String) 
+ - dbk:width (String) 
+
+[dbk:imageobject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:imagedata (dbk:imagedata) = dbk:imagedata
+ + dbk:info (dbk:info) = dbk:info
+
+[dbk:info] > argeodbk:base
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:author (dbk:author) = dbk:author *
+ + dbk:editor (dbk:editor) = dbk:editor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:orgname (dbk:orgname) = dbk:orgname *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ + dbk:title (dbk:title) = dbk:title *
+
+[dbk:inlinemediaobject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:alt (dbk:alt) = dbk:alt
+ + dbk:audioobject (dbk:audioobject) = dbk:audioobject *
+ + dbk:imageobject (dbk:imageobject) = dbk:imageobject *
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ + dbk:videoobject (dbk:videoobject) = dbk:videoobject *
+
+[dbk:itemizedlist] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:listitem (dbk:listitem) = dbk:listitem *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ - dbk:mark (String) 
+ - dbk:spacing (String) 
+
+[dbk:link] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:endterm (Reference) 
+ - dbk:xrefstyle (String) 
+
+[dbk:listitem] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ - dbk:override (String) 
+
+[dbk:mediaobject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:alt (dbk:alt) = dbk:alt
+ + dbk:audioobject (dbk:audioobject) = dbk:audioobject *
+ + dbk:caption (dbk:caption) = dbk:caption
+ + dbk:imageobject (dbk:imageobject) = dbk:imageobject *
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ + dbk:videoobject (dbk:videoobject) = dbk:videoobject *
+
+[dbk:olink] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ - dbk:localinfo (String) 
+ - dbk:targetdoc (String) 
+ - dbk:targetptr (String) 
+ - dbk:type (String) 
+ - dbk:xrefstyle (String) 
+
+[dbk:orderedlist] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:listitem (dbk:listitem) = dbk:listitem *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:remark (dbk:remark) = dbk:remark *
+ - dbk:continuation (String) 
+ - dbk:inheritnum (String) 
+ - dbk:numeration (String) 
+ - dbk:spacing (String) 
+ - dbk:startingnumber (String) 
+
+[dbk:orgdiv] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:orgname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+ - dbk:otherclass (String) 
+
+[dbk:para] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:markupInlines, argeodbk:publishingElements, argeodbk:publishingInlines, argeodbk:techDocElements, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+ + dbk:info (dbk:info) = dbk:info *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+
+[dbk:personblurb] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:paragraphElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+
+[dbk:personname] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:phrase] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:remark] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:replaceable] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+ - dbk:class (String) 
+
+[dbk:row] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:entry (dbk:entry) = dbk:entry *
+ + dbk:entrytbl (dbk:entrytbl) = dbk:entrytbl *
+ - dbk:rowsep (String) 
+ - dbk:valign (String) 
+
+[dbk:section] > argeodbk:abstractSection, argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements, argeodbk:titled
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:section (dbk:section) = dbk:section *
+
+[dbk:set] > argeodbk:base, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:book (dbk:book) = dbk:book *
+ + dbk:set (dbk:set) = dbk:set *
+ + dbk:subtitle (dbk:subtitle) = dbk:subtitle *
+ - dbk:label (String) 
+ - dbk:status (String) 
+
+[dbk:simplelist] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:columns (String) 
+ - dbk:type (String) 
+
+[dbk:spanspec] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:colsep (String) 
+ - dbk:nameend (String) 
+ - dbk:namest (String) 
+ - dbk:rowsep (String) 
+ - dbk:spanname (String) 
+
+[dbk:subscript] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:subtitle] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:superscript] > argeodbk:base, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:ubiquitousInlines
+
+[dbk:table] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:titled
+ + dbk:caption (dbk:caption) = dbk:caption
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:tbody (dbk:tbody) = dbk:tbody *
+ + dbk:textobject (dbk:textobject) = dbk:textobject *
+ + dbk:tfoot (dbk:tfoot) = dbk:tfoot
+ + dbk:tgroup (dbk:tgroup) = dbk:tgroup *
+ + dbk:thead (dbk:thead) = dbk:thead
+ - dbk:border (String) 
+ - dbk:cellpadding (String) 
+ - dbk:cellspacing (String) 
+ - dbk:class (String) 
+ - dbk:colsep (String) 
+ - dbk:floatstyle (String) 
+ - dbk:frame (String) 
+ - dbk:label (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:orient (String) 
+ - dbk:pgwide (String) 
+ - dbk:rowheader (String) 
+ - dbk:rowsep (String) 
+ - dbk:rules (String) 
+ - dbk:shortentry (String) 
+ - dbk:style (String) 
+ - dbk:summary (String) 
+ - dbk:tabstyle (String) 
+ - dbk:title (String) 
+ - dbk:tocentry (String) 
+ - dbk:width (String) 
+
+[dbk:tbody] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:row (dbk:row) = dbk:row *
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:valign (String) 
+
+[dbk:textobject] > argeodbk:base, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:listElements, argeodbk:paragraphElements, argeodbk:publishingElements, argeodbk:techDocElements
+ + dbk:anchor (dbk:anchor) = dbk:anchor *
+ + dbk:annotation (dbk:annotation) = dbk:annotation *
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:mediaobject (dbk:mediaobject) = dbk:mediaobject *
+ + dbk:phrase (dbk:phrase) = dbk:phrase
+ + dbk:remark (dbk:remark) = dbk:remark *
+
+[dbk:tfoot] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:colspec (dbk:colspec) = dbk:colspec *
+ + dbk:row (dbk:row) = dbk:row *
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:valign (String) 
+
+[dbk:tgroup] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:colspec (dbk:colspec) = dbk:colspec *
+ + dbk:spanspec (dbk:spanspec) = dbk:spanspec *
+ + dbk:tbody (dbk:tbody) = dbk:tbody
+ + dbk:tfoot (dbk:tfoot) = dbk:tfoot
+ + dbk:thead (dbk:thead) = dbk:thead
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:cols (String) 
+ - dbk:colsep (String) 
+ - dbk:rowsep (String) 
+ - dbk:tgroupstyle (String) 
+
+[dbk:thead] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:colspec (dbk:colspec) = dbk:colspec *
+ + dbk:row (dbk:row) = dbk:row *
+ - dbk:align (String) 
+ - dbk:char (String) 
+ - dbk:charoff (String) 
+ - dbk:class (String) 
+ - dbk:lang (String) 
+ - dbk:onclick (String) 
+ - dbk:ondblclick (String) 
+ - dbk:onkeydown (String) 
+ - dbk:onkeypress (String) 
+ - dbk:onkeyup (String) 
+ - dbk:onmousedown (String) 
+ - dbk:onmousemove (String) 
+ - dbk:onmouseout (String) 
+ - dbk:onmouseover (String) 
+ - dbk:onmouseup (String) 
+ - dbk:style (String) 
+ - dbk:title (String) 
+ - dbk:valign (String) 
+
+[dbk:title] > argeodbk:base, argeodbk:bibliographyInlines, argeodbk:freeText, argeodbk:indexingInlines, argeodbk:linkingAttributes, argeodbk:markupInlines, argeodbk:publishingInlines, argeodbk:techDocInlines, argeodbk:ubiquitousInlines
+
+[dbk:videodata] > argeodbk:base
+ + dbk:info (dbk:info) = dbk:info
+ - dbk:align (String) 
+ - dbk:contentdepth (String) 
+ - dbk:contentwidth (String) 
+ - dbk:depth (String) 
+ - dbk:entityref (String) 
+ - dbk:fileref (String) 
+ - dbk:format (String) 
+ - dbk:scale (String) 
+ - dbk:scalefit (String) 
+ - dbk:valign (String) 
+ - dbk:width (String) 
+
+[dbk:videoobject] > argeodbk:base, argeodbk:linkingAttributes
+ + dbk:info (dbk:info) = dbk:info
+ + dbk:videodata (dbk:videodata) = dbk:videodata
+
+[dbk:xmltext] > nt:base
+ - jcr:xmlcharacters (String) 
+
+[dbk:xref] > argeodbk:base, argeodbk:linkingAttributes
+ - dbk:endterm (Reference) 
+ - dbk:xrefstyle (String) 
+
+
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/BinaryChannel.java b/org.argeo.jcr/src/org/argeo/jcr/fs/BinaryChannel.java
new file mode 100644 (file)
index 0000000..94eb704
--- /dev/null
@@ -0,0 +1,219 @@
+package org.argeo.jcr.fs;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import javax.activation.FileTypeMap;
+import javax.activation.MimetypesFileTypeMap;
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.jcr.JcrUtils;
+
+public class BinaryChannel implements SeekableByteChannel {
+       private final Node file;
+       private Binary binary;
+       private boolean open = true;
+
+       private long position = 0;
+
+       // private ByteBuffer toWrite;
+       private FileChannel fc = null;
+
+       private static FileTypeMap fileTypeMap;
+
+       static {
+               try {
+                       fileTypeMap = new MimetypesFileTypeMap("/etc/mime.types");
+               } catch (IOException e) {
+                       fileTypeMap = FileTypeMap.getDefaultFileTypeMap();
+               }
+       }
+
+       public BinaryChannel(Node file) throws RepositoryException, IOException {
+               this.file = file;
+               // int capacity = 1024 * 1024;
+               // this.toWrite = ByteBuffer.allocate(capacity);
+               if (file.isNodeType(NodeType.NT_FILE)) {
+                       if (file.hasNode(Property.JCR_CONTENT)) {
+                               Node data = file.getNode(Property.JCR_CONTENT);
+                               this.binary = data.getProperty(Property.JCR_DATA).getBinary();
+                       } else {
+                               Node data = file.addNode(Property.JCR_CONTENT, NodeType.NT_RESOURCE);
+                               try (InputStream in = new ByteArrayInputStream(new byte[0])) {
+                                       this.binary = data.getSession().getValueFactory().createBinary(in);
+                               }
+                               data.setProperty(Property.JCR_DATA, this.binary);
+
+                               // MIME type
+                               String mime = fileTypeMap.getContentType(file.getName());
+                               data.setProperty(Property.JCR_MIMETYPE, mime);
+
+                               data.getSession().save();
+                       }
+               } else {
+                       throw new IllegalArgumentException(
+                                       "Unsupported file node " + file + " (" + file.getPrimaryNodeType() + ")");
+               }
+       }
+
+       @Override
+       public synchronized boolean isOpen() {
+               return open;
+       }
+
+       @Override
+       public synchronized void close() throws IOException {
+               if (isModified()) {
+                       Binary newBinary = null;
+                       try {
+                               Session session = file.getSession();
+                               // byte[] arr = new byte[(int) position];
+                               // toWrite.flip();
+                               // toWrite.get(arr);
+                               fc.position(0);
+                               InputStream in = Channels.newInputStream(fc);
+                               newBinary = session.getValueFactory().createBinary(in);
+                               file.getNode(Property.JCR_CONTENT).setProperty(Property.JCR_DATA, newBinary);
+                               session.save();
+                               open = false;
+                       } catch (RepositoryException e) {
+                               throw new IOException("Cannot close " + file, e);
+                       } finally {
+                               JcrUtils.closeQuietly(newBinary);
+                               // IOUtils.closeQuietly(fc);
+                               if (fc != null) {
+                                       fc.close();
+                               }
+                       }
+               } else {
+                       clearReadState();
+                       open = false;
+               }
+       }
+
+       @Override
+       public int read(ByteBuffer dst) throws IOException {
+               if (isModified()) {
+                       return fc.read(dst);
+               } else {
+
+                       try {
+                               int read;
+                               // int capacity = dst.capacity();
+                               byte[] arr = dst.array();
+                               read = binary.read(arr, position);
+                               // dst.put(arr, 0, read);
+
+                               // try {
+                               // byte[] arr = dst.array();
+                               // read = binary.read(arr, position);
+                               // } catch (UnsupportedOperationException e) {
+                               // int capacity = dst.capacity();
+                               // byte[] arr = new byte[capacity];
+                               // read = binary.read(arr, position);
+                               // dst.put(arr);
+                               // }
+                               if (read != -1)
+                                       position = position + read;
+                               return read;
+                       } catch (RepositoryException e) {
+                               throw new IOException("Cannot read into buffer", e);
+                       }
+               }
+       }
+
+       @Override
+       public int write(ByteBuffer src) throws IOException {
+               int written = getFileChannel().write(src);
+               return written;
+               // int byteCount = src.remaining();
+               // if (toWrite.remaining() < byteCount)
+               // throw new JcrFsException("Write buffer is full");
+               // toWrite.put(src);
+               // if (position < binarySize)
+               // position = binarySize + byteCount;
+               // else
+               // position = position + byteCount;
+               // return byteCount;
+       }
+
+       @Override
+       public long position() throws IOException {
+               if (isModified())
+                       return getFileChannel().position();
+               else
+                       return position;
+       }
+
+       @Override
+       public SeekableByteChannel position(long newPosition) throws IOException {
+               if (isModified()) {
+                       getFileChannel().position(position);
+               } else {
+                       this.position = newPosition;
+               }
+               return this;
+       }
+
+       @Override
+       public long size() throws IOException {
+               if (isModified()) {
+                       return getFileChannel().size();
+               } else {
+                       try {
+                               return binary.getSize();
+                       } catch (RepositoryException e) {
+                               throw new IOException("Cannot get size", e);
+                       }
+               }
+       }
+
+       @Override
+       public SeekableByteChannel truncate(long size) throws IOException {
+               getFileChannel().truncate(size);
+               // if (size != size())
+               // throw new UnsupportedOperationException("Cannot truncate JCR
+               // binary");
+               return this;
+       }
+
+       private FileChannel getFileChannel() throws IOException {
+               try {
+                       if (fc == null) {
+                               Path tempPath = Files.createTempFile(getClass().getSimpleName(), null);
+                               fc = FileChannel.open(tempPath, StandardOpenOption.WRITE, StandardOpenOption.READ,
+                                               StandardOpenOption.DELETE_ON_CLOSE, StandardOpenOption.SPARSE);
+                               ReadableByteChannel readChannel = Channels.newChannel(binary.getStream());
+                               fc.transferFrom(readChannel, 0, binary.getSize());
+                               clearReadState();
+                       }
+                       return fc;
+               } catch (RepositoryException e) {
+                       throw new IOException("Cannot get temp file channel", e);
+               }
+       }
+
+       private boolean isModified() {
+               return fc != null;
+       }
+
+       private void clearReadState() {
+               position = -1;
+               JcrUtils.closeQuietly(binary);
+               binary = null;
+       }
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java
new file mode 100644 (file)
index 0000000..92d9152
--- /dev/null
@@ -0,0 +1,117 @@
+package org.argeo.jcr.fs;
+
+import java.nio.file.attribute.FileTime;
+import java.time.Instant;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.argeo.jcr.JcrUtils;
+
+public class JcrBasicfileAttributes implements NodeFileAttributes {
+       private final Node node;
+
+       private FileTime EPOCH = FileTime.fromMillis(0);
+
+       public JcrBasicfileAttributes(Node node) {
+               if (node == null)
+                       throw new JcrFsException("Node underlying the attributes cannot be null");
+               this.node = node;
+       }
+
+       @Override
+       public FileTime lastModifiedTime() {
+               try {
+                       if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) {
+                               Instant instant = node.getProperty(Property.JCR_LAST_MODIFIED).getDate().toInstant();
+                               return FileTime.from(instant);
+                       }
+                       return EPOCH;
+               } catch (RepositoryException e) {
+                       throw new JcrFsException("Cannot get last modified time", e);
+               }
+       }
+
+       @Override
+       public FileTime lastAccessTime() {
+               return lastModifiedTime();
+       }
+
+       @Override
+       public FileTime creationTime() {
+               try {
+                       if (node.isNodeType(NodeType.MIX_CREATED)) {
+                               Instant instant = node.getProperty(Property.JCR_CREATED).getDate().toInstant();
+                               return FileTime.from(instant);
+                       }
+                       return EPOCH;
+               } catch (RepositoryException e) {
+                       throw new JcrFsException("Cannot get creation time", e);
+               }
+       }
+
+       @Override
+       public boolean isRegularFile() {
+               try {
+                       return node.isNodeType(NodeType.NT_FILE);
+               } catch (RepositoryException e) {
+                       throw new JcrFsException("Cannot check if regular file", e);
+               }
+       }
+
+       @Override
+       public boolean isDirectory() {
+               try {
+                       if (node.isNodeType(NodeType.NT_FOLDER))
+                               return true;
+                       // all other non file nodes
+                       return !(node.isNodeType(NodeType.NT_FILE) || node.isNodeType(NodeType.NT_LINKED_FILE));
+               } catch (RepositoryException e) {
+                       throw new JcrFsException("Cannot check if directory", e);
+               }
+       }
+
+       @Override
+       public boolean isSymbolicLink() {
+               try {
+                       return node.isNodeType(NodeType.NT_LINKED_FILE);
+               } catch (RepositoryException e) {
+                       throw new JcrFsException("Cannot check if linked file", e);
+               }
+       }
+
+       @Override
+       public boolean isOther() {
+               return !(isDirectory() || isRegularFile() || isSymbolicLink());
+       }
+
+       @Override
+       public long size() {
+               if (isRegularFile()) {
+                       Binary binary = null;
+                       try {
+                               binary = node.getNode(Property.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary();
+                               return binary.getSize();
+                       } catch (RepositoryException e) {
+                               throw new JcrFsException("Cannot check size", e);
+                       } finally {
+                               JcrUtils.closeQuietly(binary);
+                       }
+               }
+               return -1;
+       }
+
+       @Override
+       public Object fileKey() {
+               return null;
+       }
+
+       @Override
+       public Node getNode() {
+               return node;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileStore.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileStore.java
new file mode 100644 (file)
index 0000000..32a3ecb
--- /dev/null
@@ -0,0 +1,72 @@
+package org.argeo.jcr.fs;
+
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileStoreAttributeView;
+
+public class JcrFileStore extends FileStore {
+
+       @Override
+       public String name() {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       @Override
+       public String type() {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       @Override
+       public boolean isReadOnly() {
+               // TODO Auto-generated method stub
+               return false;
+       }
+
+       @Override
+       public long getTotalSpace() throws IOException {
+               // TODO Auto-generated method stub
+               return 0;
+       }
+
+       @Override
+       public long getUsableSpace() throws IOException {
+               // TODO Auto-generated method stub
+               return 0;
+       }
+
+       @Override
+       public long getUnallocatedSpace() throws IOException {
+               // TODO Auto-generated method stub
+               return 0;
+       }
+
+       @Override
+       public boolean supportsFileAttributeView(
+                       Class<? extends FileAttributeView> type) {
+               // TODO Auto-generated method stub
+               return false;
+       }
+
+       @Override
+       public boolean supportsFileAttributeView(String name) {
+               // TODO Auto-generated method stub
+               return false;
+       }
+
+       @Override
+       public <V extends FileStoreAttributeView> V getFileStoreAttributeView(
+                       Class<V> type) {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       @Override
+       public Object getAttribute(String attribute) throws IOException {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java
new file mode 100644 (file)
index 0000000..d11f0c5
--- /dev/null
@@ -0,0 +1,125 @@
+package org.argeo.jcr.fs;
+
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.argeo.jcr.JcrUtils;
+
+public class JcrFileSystem extends FileSystem {
+       private final JcrFileSystemProvider provider;
+       private final Session session;
+       private String userHomePath = null;
+
+       public JcrFileSystem(JcrFileSystemProvider provider, Session session) throws IOException {
+               super();
+               this.provider = provider;
+               this.session = session;
+               Node userHome = provider.getUserHome(session);
+               if (userHome != null)
+                       try {
+                               userHomePath = userHome.getPath();
+                       } catch (RepositoryException e) {
+                               throw new IOException("Cannot retrieve user home path", e);
+                       }
+       }
+
+       public String getUserHomePath() {
+               return userHomePath;
+       }
+
+       @Override
+       public FileSystemProvider provider() {
+               return provider;
+       }
+
+       @Override
+       public void close() throws IOException {
+               JcrUtils.logoutQuietly(session);
+       }
+
+       @Override
+       public boolean isOpen() {
+               return session.isLive();
+       }
+
+       @Override
+       public boolean isReadOnly() {
+               return false;
+       }
+
+       @Override
+       public String getSeparator() {
+               return "/";
+       }
+
+       @Override
+       public Iterable<Path> getRootDirectories() {
+               try {
+                       Set<Path> single = new HashSet<>();
+                       single.add(new JcrPath(this, session.getRootNode()));
+                       return single;
+               } catch (RepositoryException e) {
+                       throw new JcrFsException("Cannot get root path", e);
+               }
+       }
+
+       @Override
+       public Iterable<FileStore> getFileStores() {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public Set<String> supportedFileAttributeViews() {
+               try {
+                       String[] prefixes = session.getNamespacePrefixes();
+                       Set<String> res = new HashSet<>();
+                       for (String prefix : prefixes)
+                               res.add(prefix);
+                       res.add("basic");
+                       return res;
+               } catch (RepositoryException e) {
+                       throw new JcrFsException("Cannot get supported file attributes views", e);
+               }
+       }
+
+       @Override
+       public Path getPath(String first, String... more) {
+               StringBuilder sb = new StringBuilder(first);
+               // TODO Make it more robust
+               for (String part : more)
+                       sb.append('/').append(part);
+               return new JcrPath(this, sb.toString());
+       }
+
+       @Override
+       public PathMatcher getPathMatcher(String syntaxAndPattern) {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public UserPrincipalLookupService getUserPrincipalLookupService() {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public WatchService newWatchService() throws IOException {
+               throw new UnsupportedOperationException();
+       }
+
+       public Session getSession() {
+               return session;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java
new file mode 100644 (file)
index 0000000..04d1342
--- /dev/null
@@ -0,0 +1,299 @@
+package org.argeo.jcr.fs;
+
+import java.io.IOException;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.AccessMode;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.DirectoryStream.Filter;
+import java.nio.file.FileStore;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.PropertyDefinition;
+
+import org.argeo.jcr.JcrUtils;
+
+public abstract class JcrFileSystemProvider extends FileSystemProvider {
+
+       @Override
+       public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
+                       throws IOException {
+               Node node = toNode(path);
+               try {
+                       if (node == null) {
+                               Node parent = toNode(path.getParent());
+                               if (parent == null)
+                                       throw new IOException("No parent directory for " + path);
+                               if (parent.getPrimaryNodeType().isNodeType(NodeType.NT_FILE)
+                                               || parent.getPrimaryNodeType().isNodeType(NodeType.NT_LINKED_FILE))
+                                       throw new IOException(path + " parent is a file");
+
+                               String fileName = path.getFileName().toString();
+                               fileName = Text.escapeIllegalJcrChars(fileName);
+                               node = parent.addNode(fileName, NodeType.NT_FILE);
+                               node.addMixin(NodeType.MIX_CREATED);
+                               node.addMixin(NodeType.MIX_LAST_MODIFIED);
+                       }
+                       if (!node.isNodeType(NodeType.NT_FILE))
+                               throw new UnsupportedOperationException(node + " must be a file");
+                       return new BinaryChannel(node);
+               } catch (RepositoryException e) {
+                       discardChanges(node);
+                       throw new IOException("Cannot read file", e);
+               }
+       }
+
+       @Override
+       public DirectoryStream<Path> newDirectoryStream(Path dir, Filter<? super Path> filter) throws IOException {
+               try {
+                       Node base = toNode(dir);
+                       return new NodeDirectoryStream((JcrFileSystem) dir.getFileSystem(), base.getNodes(), filter);
+               } catch (RepositoryException e) {
+                       throw new IOException("Cannot list directory", e);
+               }
+       }
+
+       @Override
+       public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
+               Node node = toNode(dir);
+               try {
+                       if (node == null) {
+                               Node parent = toNode(dir.getParent());
+                               if (parent == null)
+                                       throw new IOException("Parent of " + dir + " does not exist");
+                               if (parent.getPrimaryNodeType().isNodeType(NodeType.NT_FILE)
+                                               || parent.getPrimaryNodeType().isNodeType(NodeType.NT_LINKED_FILE))
+                                       throw new IOException(dir + " parent is a file");
+                               String fileName = dir.getFileName().toString();
+                               fileName = Text.escapeIllegalJcrChars(fileName);
+                               node = parent.addNode(fileName, NodeType.NT_FOLDER);
+                               node.addMixin(NodeType.MIX_CREATED);
+                               node.addMixin(NodeType.MIX_LAST_MODIFIED);
+                               node.getSession().save();
+                       } else {
+                               // if (!node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER))
+                               // throw new FileExistsException(dir + " exists and is not a directory");
+                       }
+               } catch (RepositoryException e) {
+                       discardChanges(node);
+                       throw new IOException("Cannot create directory " + dir, e);
+               }
+       }
+
+       @Override
+       public void delete(Path path) throws IOException {
+               Node node = toNode(path);
+               try {
+                       if (node == null)
+                               throw new NoSuchFileException(path + " does not exist");
+                       Session session = node.getSession();
+                       if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FILE))
+                               node.remove();
+                       else if (node.getPrimaryNodeType().isNodeType(NodeType.NT_FOLDER)) {
+                               if (node.hasNodes())// TODO check only files
+                                       throw new DirectoryNotEmptyException(path.toString());
+                               node.remove();
+                       }
+                       session.save();
+               } catch (RepositoryException e) {
+                       discardChanges(node);
+                       throw new IOException("Cannot delete " + path, e);
+               }
+
+       }
+
+       @Override
+       public void copy(Path source, Path target, CopyOption... options) throws IOException {
+               Node sourceNode = toNode(source);
+               Node targetNode = toNode(target);
+               try {
+                       JcrUtils.copy(sourceNode, targetNode);
+                       sourceNode.getSession().save();
+               } catch (RepositoryException e) {
+                       discardChanges(sourceNode);
+                       discardChanges(targetNode);
+                       throw new IOException("Cannot copy from " + source + " to " + target, e);
+               }
+       }
+
+       @Override
+       public void move(Path source, Path target, CopyOption... options) throws IOException {
+               Node sourceNode = toNode(source);
+               try {
+                       Session session = sourceNode.getSession();
+                       session.move(sourceNode.getPath(), target.toString());
+                       session.save();
+               } catch (RepositoryException e) {
+                       discardChanges(sourceNode);
+                       throw new IOException("Cannot move from " + source + " to " + target, e);
+               }
+       }
+
+       @Override
+       public boolean isSameFile(Path path, Path path2) throws IOException {
+               if (path.getFileSystem() != path2.getFileSystem())
+                       return false;
+               boolean equ = path.equals(path2);
+               if (equ)
+                       return true;
+               else {
+                       try {
+                               Node node = toNode(path);
+                               Node node2 = toNode(path2);
+                               return node.isSame(node2);
+                       } catch (RepositoryException e) {
+                               throw new IOException("Cannot check whether " + path + " and " + path2 + " are same", e);
+                       }
+               }
+
+       }
+
+       @Override
+       public boolean isHidden(Path path) throws IOException {
+               return false;
+       }
+
+       @Override
+       public FileStore getFileStore(Path path) throws IOException {
+               Session session = ((JcrFileSystem) path.getFileSystem()).getSession();
+               return new WorkSpaceFileStore(session.getWorkspace());
+       }
+
+       @Override
+       public void checkAccess(Path path, AccessMode... modes) throws IOException {
+               try {
+                       Session session = ((JcrFileSystem) path.getFileSystem()).getSession();
+                       if (!session.itemExists(path.toString()))
+                               throw new NoSuchFileException(path + " does not exist");
+                       // TODO check access via JCR api
+               } catch (RepositoryException e) {
+                       throw new IOException("Cannot delete " + path, e);
+               }
+       }
+
+       @Override
+       public <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) {
+               throw new UnsupportedOperationException();
+       }
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options)
+                       throws IOException {
+               // TODO check if assignable
+               Node node = toNode(path);
+               if (node == null) {
+                       throw new IOException("JCR node not found for " + path);
+               }
+               return (A) new JcrBasicfileAttributes(node);
+       }
+
+       @Override
+       public Map<String, Object> readAttributes(Path path, String attributes, LinkOption... options) throws IOException {
+               try {
+                       Node node = toNode(path);
+                       String pattern = attributes.replace(',', '|');
+                       Map<String, Object> res = new HashMap<String, Object>();
+                       PropertyIterator it = node.getProperties(pattern);
+                       props: while (it.hasNext()) {
+                               Property prop = it.nextProperty();
+                               PropertyDefinition pd = prop.getDefinition();
+                               if (pd.isMultiple())
+                                       continue props;
+                               int requiredType = pd.getRequiredType();
+                               switch (requiredType) {
+                               case PropertyType.LONG:
+                                       res.put(prop.getName(), prop.getLong());
+                                       break;
+                               case PropertyType.DOUBLE:
+                                       res.put(prop.getName(), prop.getDouble());
+                                       break;
+                               case PropertyType.BOOLEAN:
+                                       res.put(prop.getName(), prop.getBoolean());
+                                       break;
+                               case PropertyType.DATE:
+                                       res.put(prop.getName(), prop.getDate());
+                                       break;
+                               case PropertyType.BINARY:
+                                       byte[] arr = JcrUtils.getBinaryAsBytes(prop);
+                                       res.put(prop.getName(), arr);
+                                       break;
+                               default:
+                                       res.put(prop.getName(), prop.getString());
+                               }
+                       }
+                       return res;
+               } catch (RepositoryException e) {
+                       throw new IOException("Cannot read attributes of " + path, e);
+               }
+       }
+
+       @Override
+       public void setAttribute(Path path, String attribute, Object value, LinkOption... options) throws IOException {
+               Node node = toNode(path);
+               try {
+                       if (value instanceof byte[]) {
+                               JcrUtils.setBinaryAsBytes(node, attribute, (byte[]) value);
+                       } else if (value instanceof Calendar) {
+                               node.setProperty(attribute, (Calendar) value);
+                       } else {
+                               node.setProperty(attribute, value.toString());
+                       }
+                       node.getSession().save();
+               } catch (RepositoryException e) {
+                       discardChanges(node);
+                       throw new IOException("Cannot set attribute " + attribute + " on " + path, e);
+               }
+       }
+
+       protected Node toNode(Path path) {
+               try {
+                       return ((JcrPath) path).getNode();
+               } catch (RepositoryException e) {
+                       throw new JcrFsException("Cannot convert path " + path + " to JCR Node", e);
+               }
+       }
+
+       /** Discard changes in the underlying session */
+       protected void discardChanges(Node node) {
+               if (node == null)
+                       return;
+               try {
+                       // discard changes
+                       node.getSession().refresh(false);
+               } catch (RepositoryException e) {
+                       e.printStackTrace();
+                       // TODO log out session?
+                       // TODO use Commons logging?
+               }
+       }
+
+       /**
+        * To be overriden in order to support the ~ path, with an implementation
+        * specific concept of user home.
+        * 
+        * @return null by default
+        */
+       public Node getUserHome(Session session) {
+               return null;
+       }
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFsException.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrFsException.java
new file mode 100644 (file)
index 0000000..f214fdc
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.jcr.fs;
+
+
+/** Exception related to the JCR FS */
+public class JcrFsException extends RuntimeException {
+       private static final long serialVersionUID = -7973896038244922980L;
+
+       public JcrFsException(String message, Throwable e) {
+               super(message, e);
+       }
+
+       public JcrFsException(String message) {
+               super(message);
+       }
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java b/org.argeo.jcr/src/org/argeo/jcr/fs/JcrPath.java
new file mode 100644 (file)
index 0000000..b75189a
--- /dev/null
@@ -0,0 +1,364 @@
+package org.argeo.jcr.fs;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.FileSystem;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent.Kind;
+import java.nio.file.WatchEvent.Modifier;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+public class JcrPath implements Path {
+       private final static String delimStr = "/";
+       private final static char delimChar = '/';
+
+       private final JcrFileSystem fs;
+       private final String[] path;// null means root
+       private final boolean absolute;
+
+       // optim
+       private final int hashCode;
+
+       public JcrPath(JcrFileSystem filesSystem, String path) {
+               this.fs = filesSystem;
+               if (path == null)
+                       throw new JcrFsException("Path cannot be null");
+               if (path.equals(delimStr)) {// root
+                       this.path = null;
+                       this.absolute = true;
+                       this.hashCode = 0;
+                       return;
+               } else if (path.equals("")) {// empty path
+                       this.path = new String[] { "" };
+                       this.absolute = false;
+                       this.hashCode = "".hashCode();
+                       return;
+               }
+
+               if (path.equals("~")) {// home
+                       path = filesSystem.getUserHomePath();
+                       if (path == null)
+                               throw new JcrFsException("No home directory available");
+               }
+
+               this.absolute = path.charAt(0) == delimChar ? true : false;
+               String trimmedPath = path.substring(absolute ? 1 : 0,
+                               path.charAt(path.length() - 1) == delimChar ? path.length() - 1 : path.length());
+               this.path = trimmedPath.split(delimStr);
+               for (int i = 0; i < this.path.length; i++) {
+                       this.path[i] = Text.unescapeIllegalJcrChars(this.path[i]);
+               }
+               this.hashCode = this.path[this.path.length - 1].hashCode();
+       }
+
+       public JcrPath(JcrFileSystem filesSystem, Node node) throws RepositoryException {
+               this(filesSystem, node.getPath());
+       }
+
+       /** Internal optimisation */
+       private JcrPath(JcrFileSystem filesSystem, String[] path, boolean absolute) {
+               this.fs = filesSystem;
+               this.path = path;
+               this.absolute = path == null ? true : absolute;
+               this.hashCode = path == null ? 0 : path[path.length - 1].hashCode();
+       }
+
+       @Override
+       public FileSystem getFileSystem() {
+               return fs;
+       }
+
+       @Override
+       public boolean isAbsolute() {
+               return absolute;
+       }
+
+       @Override
+       public Path getRoot() {
+               try {
+                       if (path == null)
+                               return this;
+                       return new JcrPath(fs, fs.getSession().getRootNode());
+               } catch (RepositoryException e) {
+                       throw new JcrFsException("Cannot get root", e);
+               }
+       }
+
+       @Override
+       public String toString() {
+               if (path == null)
+                       return "/";
+               StringBuilder sb = new StringBuilder();
+               if (isAbsolute())
+                       sb.append('/');
+               for (int i = 0; i < path.length; i++) {
+                       if (i != 0)
+                               sb.append('/');
+                       sb.append(path[i]);
+               }
+               return sb.toString();
+       }
+
+       public String toJcrPath() {
+               if (path == null)
+                       return "/";
+               StringBuilder sb = new StringBuilder();
+               if (isAbsolute())
+                       sb.append('/');
+               for (int i = 0; i < path.length; i++) {
+                       if (i != 0)
+                               sb.append('/');
+                       sb.append(Text.escapeIllegalJcrChars(path[i]));
+               }
+               return sb.toString();
+       }
+
+       @Override
+       public Path getFileName() {
+               if (path == null)
+                       return null;
+               return new JcrPath(fs, path[path.length - 1]);
+       }
+
+       @Override
+       public Path getParent() {
+               if (path == null)
+                       return null;
+               if (path.length == 1)// root
+                       return new JcrPath(fs, delimStr);
+               String[] parentPath = Arrays.copyOfRange(path, 0, path.length - 1);
+               return new JcrPath(fs, parentPath, absolute);
+       }
+
+       @Override
+       public int getNameCount() {
+               if (path == null)
+                       return 0;
+               return path.length;
+       }
+
+       @Override
+       public Path getName(int index) {
+               if (path == null)
+                       return null;
+               return new JcrPath(fs, path[index]);
+       }
+
+       @Override
+       public Path subpath(int beginIndex, int endIndex) {
+               if (path == null)
+                       return null;
+               String[] parentPath = Arrays.copyOfRange(path, beginIndex, endIndex);
+               return new JcrPath(fs, parentPath, false);
+       }
+
+       @Override
+       public boolean startsWith(Path other) {
+               return toString().startsWith(other.toString());
+       }
+
+       @Override
+       public boolean startsWith(String other) {
+               return toString().startsWith(other);
+       }
+
+       @Override
+       public boolean endsWith(Path other) {
+               return toString().endsWith(other.toString());
+       }
+
+       @Override
+       public boolean endsWith(String other) {
+               return toString().endsWith(other);
+       }
+
+       @Override
+       public Path normalize() {
+               // always normalized
+               return this;
+       }
+
+       @Override
+       public Path resolve(Path other) {
+               JcrPath otherPath = (JcrPath) other;
+               if (otherPath.isAbsolute())
+                       return other;
+               String[] newPath;
+               if (path == null) {
+                       newPath = new String[otherPath.path.length];
+                       System.arraycopy(otherPath.path, 0, newPath, 0, otherPath.path.length);
+               } else {
+                       newPath = new String[path.length + otherPath.path.length];
+                       System.arraycopy(path, 0, newPath, 0, path.length);
+                       System.arraycopy(otherPath.path, 0, newPath, path.length, otherPath.path.length);
+               }
+               return new JcrPath(fs, newPath, absolute);
+       }
+
+       @Override
+       public final Path resolve(String other) {
+               return resolve(getFileSystem().getPath(other));
+       }
+
+       @Override
+       public final Path resolveSibling(Path other) {
+               if (other == null)
+                       throw new NullPointerException();
+               Path parent = getParent();
+               return (parent == null) ? other : parent.resolve(other);
+       }
+
+       @Override
+       public final Path resolveSibling(String other) {
+               return resolveSibling(getFileSystem().getPath(other));
+       }
+
+       @Override
+       public final Iterator<Path> iterator() {
+               return new Iterator<Path>() {
+                       private int i = 0;
+
+                       @Override
+                       public boolean hasNext() {
+                               return (i < getNameCount());
+                       }
+
+                       @Override
+                       public Path next() {
+                               if (i < getNameCount()) {
+                                       Path result = getName(i);
+                                       i++;
+                                       return result;
+                               } else {
+                                       throw new NoSuchElementException();
+                               }
+                       }
+
+                       @Override
+                       public void remove() {
+                               throw new UnsupportedOperationException();
+                       }
+               };
+       }
+
+       @Override
+       public Path relativize(Path other) {
+               if (equals(other))
+                       return new JcrPath(fs, "");
+               if (other.startsWith(this)) {
+                       String p1 = toString();
+                       String p2 = other.toString();
+                       String relative =  p2.substring(p1.length(), p2.length());
+                       if(relative.charAt(0)=='/')
+                               relative = relative.substring(1);
+                       return new JcrPath(fs,relative);
+               }
+               throw new IllegalArgumentException(other + " cannot be relativized against " + this);
+       }
+
+       @Override
+       public URI toUri() {
+               try {
+                       return new URI("jcr", toString(), null);
+               } catch (URISyntaxException e) {
+                       throw new JcrFsException("Cannot create URI for " + toString(), e);
+               }
+       }
+
+       @Override
+       public Path toAbsolutePath() {
+               if (isAbsolute())
+                       return this;
+               return new JcrPath(fs, path, true);
+       }
+
+       @Override
+       public Path toRealPath(LinkOption... options) throws IOException {
+               return this;
+       }
+
+       @Override
+       public File toFile() {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override
+       public WatchKey register(WatchService watcher, Kind<?>[] events, Modifier... modifiers) throws IOException {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       @Override
+       public WatchKey register(WatchService watcher, Kind<?>... events) throws IOException {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       @Override
+       public int compareTo(Path other) {
+               return toString().compareTo(other.toString());
+       }
+
+       public Node getNode() throws RepositoryException {
+               if (!isAbsolute())// TODO default dir
+                       throw new JcrFsException("Cannot get node from relative path");
+               String pathStr = toJcrPath();
+               Session session = fs.getSession();
+               // TODO synchronize on the session ?
+               if (!session.itemExists(pathStr))
+                       return null;
+               return session.getNode(pathStr);
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (!(obj instanceof JcrPath))
+                       return false;
+               JcrPath other = (JcrPath) obj;
+
+               if (path == null) {// root
+                       if (other.path == null)// root
+                               return true;
+                       else
+                               return false;
+               } else {
+                       if (other.path == null)// root
+                               return false;
+               }
+               // non root
+               if (path.length != other.path.length)
+                       return false;
+               for (int i = 0; i < path.length; i++) {
+                       if (!path[i].equals(other.path[i]))
+                               return false;
+               }
+               return true;
+       }
+
+       @Override
+       public int hashCode() {
+               return hashCode;
+       }
+
+       @Override
+       protected Object clone() throws CloneNotSupportedException {
+               return new JcrPath(fs, toString());
+       }
+
+       @Override
+       protected void finalize() throws Throwable {
+               Arrays.fill(path, null);
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java b/org.argeo.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java
new file mode 100644 (file)
index 0000000..892aaee
--- /dev/null
@@ -0,0 +1,66 @@
+package org.argeo.jcr.fs;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+import java.util.Iterator;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+
+public class NodeDirectoryStream implements DirectoryStream<Path> {
+       private final JcrFileSystem fs;
+       private final NodeIterator nodeIterator;
+       private final Filter<? super Path> filter;
+
+       public NodeDirectoryStream(JcrFileSystem fs, NodeIterator nodeIterator, Filter<? super Path> filter) {
+               this.fs = fs;
+               this.nodeIterator = nodeIterator;
+               this.filter = filter;
+       }
+
+       @Override
+       public void close() throws IOException {
+       }
+
+       @Override
+       public Iterator<Path> iterator() {
+               return new Iterator<Path>() {
+                       private JcrPath next = null;
+
+                       @Override
+                       public synchronized boolean hasNext() {
+                               if (next != null)
+                                       return true;
+                               nodes: while (nodeIterator.hasNext()) {
+                                       try {
+                                               Node node = nodeIterator.nextNode();
+                                               String nodeName = node.getName();
+                                               if (nodeName.startsWith("rep:") || nodeName.startsWith("jcr:"))
+                                                       continue nodes;
+                                               next = new JcrPath(fs, node);
+                                               if (filter != null) {
+                                                       if (filter.accept(next))
+                                                               break nodes;
+                                               } else
+                                                       break nodes;
+                                       } catch (Exception e) {
+                                               throw new JcrFsException("Could not get next path", e);
+                                       }
+                               }
+                               return next != null;
+                       }
+
+                       @Override
+                       public synchronized Path next() {
+                               if (!hasNext())// make sure has next has been called
+                                       return null;
+                               JcrPath res = next;
+                               next = null;
+                               return res;
+                       }
+
+               };
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java b/org.argeo.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java
new file mode 100644 (file)
index 0000000..8054d52
--- /dev/null
@@ -0,0 +1,9 @@
+package org.argeo.jcr.fs;
+
+import java.nio.file.attribute.BasicFileAttributes;
+
+import javax.jcr.Node;
+
+public interface NodeFileAttributes extends BasicFileAttributes {
+       public Node getNode();
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/SessionFsProvider.java b/org.argeo.jcr/src/org/argeo/jcr/fs/SessionFsProvider.java
new file mode 100644 (file)
index 0000000..3c5bf16
--- /dev/null
@@ -0,0 +1,58 @@
+package org.argeo.jcr.fs;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.Path;
+import java.util.Map;
+
+import javax.jcr.Session;
+
+/** An FS provider based on a single JCR session (experimental). */
+public class SessionFsProvider extends JcrFileSystemProvider {
+       private Session session;
+       private JcrFileSystem fileSystem;
+
+       public SessionFsProvider(Session session) {
+               this.session = session;
+       }
+
+       @Override
+       public String getScheme() {
+               return "jcr+session";
+       }
+
+       @Override
+       public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
+               if (fileSystem != null && fileSystem.isOpen())
+                       throw new FileSystemAlreadyExistsException();
+               fileSystem = new JcrFileSystem(this, session) {
+                       boolean open;
+
+                       @Override
+                       public void close() throws IOException {
+                               // prevent the session logout
+                               open = false;
+                       }
+
+                       @Override
+                       public boolean isOpen() {
+                               return open;
+                       }
+
+               };
+               return fileSystem;
+       }
+
+       @Override
+       public FileSystem getFileSystem(URI uri) {
+               return fileSystem;
+       }
+
+       @Override
+       public Path getPath(URI uri) {
+               return new JcrPath(fileSystem, uri.getPath());
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/Text.java b/org.argeo.jcr/src/org/argeo/jcr/fs/Text.java
new file mode 100644 (file)
index 0000000..4643c8c
--- /dev/null
@@ -0,0 +1,877 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr.fs;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Properties;
+
+/**
+ * <b>Hacked from org.apache.jackrabbit.util.Text in Jackrabbit JCR Commons</b>
+ * This Class provides some text related utilities
+ */
+class Text {
+
+       /**
+        * Hidden constructor.
+        */
+       private Text() {
+       }
+
+       /**
+        * used for the md5
+        */
+       public static final char[] hexTable = "0123456789abcdef".toCharArray();
+
+       /**
+        * Calculate an MD5 hash of the string given.
+        *
+        * @param data
+        *            the data to encode
+        * @param enc
+        *            the character encoding to use
+        * @return a hex encoded string of the md5 digested input
+        */
+       public static String md5(String data, String enc) throws UnsupportedEncodingException {
+               try {
+                       return digest("MD5", data.getBytes(enc));
+               } catch (NoSuchAlgorithmException e) {
+                       throw new InternalError("MD5 digest not available???");
+               }
+       }
+
+       /**
+        * Calculate an MD5 hash of the string given using 'utf-8' encoding.
+        *
+        * @param data
+        *            the data to encode
+        * @return a hex encoded string of the md5 digested input
+        */
+       public static String md5(String data) {
+               try {
+                       return md5(data, "utf-8");
+               } catch (UnsupportedEncodingException e) {
+                       throw new InternalError("UTF8 digest not available???");
+               }
+       }
+
+       /**
+        * Digest the plain string using the given algorithm.
+        *
+        * @param algorithm
+        *            The alogrithm for the digest. This algorithm must be supported
+        *            by the MessageDigest class.
+        * @param data
+        *            The plain text String to be digested.
+        * @param enc
+        *            The character encoding to use
+        * @return The digested plain text String represented as Hex digits.
+        * @throws java.security.NoSuchAlgorithmException
+        *             if the desired algorithm is not supported by the
+        *             MessageDigest class.
+        * @throws java.io.UnsupportedEncodingException
+        *             if the encoding is not supported
+        */
+       public static String digest(String algorithm, String data, String enc)
+                       throws NoSuchAlgorithmException, UnsupportedEncodingException {
+
+               return digest(algorithm, data.getBytes(enc));
+       }
+
+       /**
+        * Digest the plain string using the given algorithm.
+        *
+        * @param algorithm
+        *            The algorithm for the digest. This algorithm must be supported
+        *            by the MessageDigest class.
+        * @param data
+        *            the data to digest with the given algorithm
+        * @return The digested plain text String represented as Hex digits.
+        * @throws java.security.NoSuchAlgorithmException
+        *             if the desired algorithm is not supported by the
+        *             MessageDigest class.
+        */
+       public static String digest(String algorithm, byte[] data) throws NoSuchAlgorithmException {
+
+               MessageDigest md = MessageDigest.getInstance(algorithm);
+               byte[] digest = md.digest(data);
+               StringBuilder res = new StringBuilder(digest.length * 2);
+               for (byte b : digest) {
+                       res.append(hexTable[(b >> 4) & 15]);
+                       res.append(hexTable[b & 15]);
+               }
+               return res.toString();
+       }
+
+       /**
+        * returns an array of strings decomposed of the original string, split at
+        * every occurrence of 'ch'. if 2 'ch' follow each other with no
+        * intermediate characters, empty "" entries are avoided.
+        *
+        * @param str
+        *            the string to decompose
+        * @param ch
+        *            the character to use a split pattern
+        * @return an array of strings
+        */
+       public static String[] explode(String str, int ch) {
+               return explode(str, ch, false);
+       }
+
+       /**
+        * returns an array of strings decomposed of the original string, split at
+        * every occurrence of 'ch'.
+        *
+        * @param str
+        *            the string to decompose
+        * @param ch
+        *            the character to use a split pattern
+        * @param respectEmpty
+        *            if <code>true</code>, empty elements are generated
+        * @return an array of strings
+        */
+       public static String[] explode(String str, int ch, boolean respectEmpty) {
+               if (str == null || str.length() == 0) {
+                       return new String[0];
+               }
+
+               ArrayList<String> strings = new ArrayList<String>();
+               int pos;
+               int lastpos = 0;
+
+               // add snipples
+               while ((pos = str.indexOf(ch, lastpos)) >= 0) {
+                       if (pos - lastpos > 0 || respectEmpty) {
+                               strings.add(str.substring(lastpos, pos));
+                       }
+                       lastpos = pos + 1;
+               }
+               // add rest
+               if (lastpos < str.length()) {
+                       strings.add(str.substring(lastpos));
+               } else if (respectEmpty && lastpos == str.length()) {
+                       strings.add("");
+               }
+
+               // return string array
+               return strings.toArray(new String[strings.size()]);
+       }
+
+       /**
+        * Concatenates all strings in the string array using the specified
+        * delimiter.
+        * 
+        * @param arr
+        * @param delim
+        * @return the concatenated string
+        */
+       public static String implode(String[] arr, String delim) {
+               StringBuilder buf = new StringBuilder();
+               for (int i = 0; i < arr.length; i++) {
+                       if (i > 0) {
+                               buf.append(delim);
+                       }
+                       buf.append(arr[i]);
+               }
+               return buf.toString();
+       }
+
+       /**
+        * Replaces all occurrences of <code>oldString</code> in <code>text</code>
+        * with <code>newString</code>.
+        *
+        * @param text
+        * @param oldString
+        *            old substring to be replaced with <code>newString</code>
+        * @param newString
+        *            new substring to replace occurrences of <code>oldString</code>
+        * @return a string
+        */
+       public static String replace(String text, String oldString, String newString) {
+               if (text == null || oldString == null || newString == null) {
+                       throw new IllegalArgumentException("null argument");
+               }
+               int pos = text.indexOf(oldString);
+               if (pos == -1) {
+                       return text;
+               }
+               int lastPos = 0;
+               StringBuilder sb = new StringBuilder(text.length());
+               while (pos != -1) {
+                       sb.append(text.substring(lastPos, pos));
+                       sb.append(newString);
+                       lastPos = pos + oldString.length();
+                       pos = text.indexOf(oldString, lastPos);
+               }
+               if (lastPos < text.length()) {
+                       sb.append(text.substring(lastPos));
+               }
+               return sb.toString();
+       }
+
+       /**
+        * Replaces XML characters in the given string that might need escaping as
+        * XML text or attribute
+        *
+        * @param text
+        *            text to be escaped
+        * @return a string
+        */
+       public static String encodeIllegalXMLCharacters(String text) {
+               return encodeMarkupCharacters(text, false);
+       }
+
+       /**
+        * Replaces HTML characters in the given string that might need escaping as
+        * HTML text or attribute
+        *
+        * @param text
+        *            text to be escaped
+        * @return a string
+        */
+       public static String encodeIllegalHTMLCharacters(String text) {
+               return encodeMarkupCharacters(text, true);
+       }
+
+       private static String encodeMarkupCharacters(String text, boolean isHtml) {
+               if (text == null) {
+                       throw new IllegalArgumentException("null argument");
+               }
+               StringBuilder buf = null;
+               int length = text.length();
+               int pos = 0;
+               for (int i = 0; i < length; i++) {
+                       int ch = text.charAt(i);
+                       switch (ch) {
+                       case '<':
+                       case '>':
+                       case '&':
+                       case '"':
+                       case '\'':
+                               if (buf == null) {
+                                       buf = new StringBuilder();
+                               }
+                               if (i > 0) {
+                                       buf.append(text.substring(pos, i));
+                               }
+                               pos = i + 1;
+                               break;
+                       default:
+                               continue;
+                       }
+                       if (ch == '<') {
+                               buf.append("&lt;");
+                       } else if (ch == '>') {
+                               buf.append("&gt;");
+                       } else if (ch == '&') {
+                               buf.append("&amp;");
+                       } else if (ch == '"') {
+                               buf.append("&quot;");
+                       } else if (ch == '\'') {
+                               buf.append(isHtml ? "&#39;" : "&apos;");
+                       }
+               }
+               if (buf == null) {
+                       return text;
+               } else {
+                       if (pos < length) {
+                               buf.append(text.substring(pos));
+                       }
+                       return buf.toString();
+               }
+       }
+
+       /**
+        * The list of characters that are not encoded by the <code>escape()</code>
+        * and <code>unescape()</code> METHODS. They contains the characters as
+        * defined 'unreserved' in section 2.3 of the RFC 2396 'URI generic syntax':
+        * <p>
+        * 
+        * <pre>
+        * unreserved  = alphanum | mark
+        * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
+        * </pre>
+        */
+       public static BitSet URISave;
+
+       /**
+        * Same as {@link #URISave} but also contains the '/'
+        */
+       public static BitSet URISaveEx;
+
+       static {
+               URISave = new BitSet(256);
+               int i;
+               for (i = 'a'; i <= 'z'; i++) {
+                       URISave.set(i);
+               }
+               for (i = 'A'; i <= 'Z'; i++) {
+                       URISave.set(i);
+               }
+               for (i = '0'; i <= '9'; i++) {
+                       URISave.set(i);
+               }
+               URISave.set('-');
+               URISave.set('_');
+               URISave.set('.');
+               URISave.set('!');
+               URISave.set('~');
+               URISave.set('*');
+               URISave.set('\'');
+               URISave.set('(');
+               URISave.set(')');
+
+               URISaveEx = (BitSet) URISave.clone();
+               URISaveEx.set('/');
+       }
+
+       /**
+        * Does an URL encoding of the <code>string</code> using the
+        * <code>escape</code> character. The characters that don't need encoding
+        * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax'
+        * RFC 2396, but without the escape character.
+        *
+        * @param string
+        *            the string to encode.
+        * @param escape
+        *            the escape character.
+        * @return the escaped string
+        * @throws NullPointerException
+        *             if <code>string</code> is <code>null</code>.
+        */
+       public static String escape(String string, char escape) {
+               return escape(string, escape, false);
+       }
+
+       /**
+        * Does an URL encoding of the <code>string</code> using the
+        * <code>escape</code> character. The characters that don't need encoding
+        * are those defined 'unreserved' in section 2.3 of the 'URI generic syntax'
+        * RFC 2396, but without the escape character. If <code>isPath</code> is
+        * <code>true</code>, additionally the slash '/' is ignored, too.
+        *
+        * @param string
+        *            the string to encode.
+        * @param escape
+        *            the escape character.
+        * @param isPath
+        *            if <code>true</code>, the string is treated as path
+        * @return the escaped string
+        * @throws NullPointerException
+        *             if <code>string</code> is <code>null</code>.
+        */
+       public static String escape(String string, char escape, boolean isPath) {
+               try {
+                       BitSet validChars = isPath ? URISaveEx : URISave;
+                       byte[] bytes = string.getBytes("utf-8");
+                       StringBuilder out = new StringBuilder(bytes.length);
+                       for (byte aByte : bytes) {
+                               int c = aByte & 0xff;
+                               if (validChars.get(c) && c != escape) {
+                                       out.append((char) c);
+                               } else {
+                                       out.append(escape);
+                                       out.append(hexTable[(c >> 4) & 0x0f]);
+                                       out.append(hexTable[(c) & 0x0f]);
+                               }
+                       }
+                       return out.toString();
+               } catch (UnsupportedEncodingException e) {
+                       throw new InternalError(e.toString());
+               }
+       }
+
+       /**
+        * Does a URL encoding of the <code>string</code>. The characters that don't
+        * need encoding are those defined 'unreserved' in section 2.3 of the 'URI
+        * generic syntax' RFC 2396.
+        *
+        * @param string
+        *            the string to encode
+        * @return the escaped string
+        * @throws NullPointerException
+        *             if <code>string</code> is <code>null</code>.
+        */
+       public static String escape(String string) {
+               return escape(string, '%');
+       }
+
+       /**
+        * Does a URL encoding of the <code>path</code>. The characters that don't
+        * need encoding are those defined 'unreserved' in section 2.3 of the 'URI
+        * generic syntax' RFC 2396. In contrast to the {@link #escape(String)}
+        * method, not the entire path string is escaped, but every individual part
+        * (i.e. the slashes are not escaped).
+        *
+        * @param path
+        *            the path to encode
+        * @return the escaped path
+        * @throws NullPointerException
+        *             if <code>path</code> is <code>null</code>.
+        */
+       public static String escapePath(String path) {
+               return escape(path, '%', true);
+       }
+
+       /**
+        * Does a URL decoding of the <code>string</code> using the
+        * <code>escape</code> character. Please note that in opposite to the
+        * {@link java.net.URLDecoder} it does not transform the + into spaces.
+        *
+        * @param string
+        *            the string to decode
+        * @param escape
+        *            the escape character
+        * @return the decoded string
+        * @throws NullPointerException
+        *             if <code>string</code> is <code>null</code>.
+        * @throws IllegalArgumentException
+        *             if the 2 characters following the escape character do not
+        *             represent a hex-number or if not enough characters follow an
+        *             escape character
+        */
+       public static String unescape(String string, char escape) {
+               try {
+                       byte[] utf8 = string.getBytes("utf-8");
+
+                       // Check whether escape occurs at invalid position
+                       if ((utf8.length >= 1 && utf8[utf8.length - 1] == escape)
+                                       || (utf8.length >= 2 && utf8[utf8.length - 2] == escape)) {
+                               throw new IllegalArgumentException("Premature end of escape sequence at end of input");
+                       }
+
+                       ByteArrayOutputStream out = new ByteArrayOutputStream(utf8.length);
+                       for (int k = 0; k < utf8.length; k++) {
+                               byte b = utf8[k];
+                               if (b == escape) {
+                                       out.write((decodeDigit(utf8[++k]) << 4) + decodeDigit(utf8[++k]));
+                               } else {
+                                       out.write(b);
+                               }
+                       }
+
+                       return new String(out.toByteArray(), "utf-8");
+               } catch (UnsupportedEncodingException e) {
+                       throw new InternalError(e.toString());
+               }
+       }
+
+       /**
+        * Does a URL decoding of the <code>string</code>. Please note that in
+        * opposite to the {@link java.net.URLDecoder} it does not transform the +
+        * into spaces.
+        *
+        * @param string
+        *            the string to decode
+        * @return the decoded string
+        * @throws NullPointerException
+        *             if <code>string</code> is <code>null</code>.
+        * @throws ArrayIndexOutOfBoundsException
+        *             if not enough character follow an escape character
+        * @throws IllegalArgumentException
+        *             if the 2 characters following the escape character do not
+        *             represent a hex-number.
+        */
+       public static String unescape(String string) {
+               return unescape(string, '%');
+       }
+
+       /**
+        * Escapes all illegal JCR name characters of a string. The encoding is
+        * loosely modeled after URI encoding, but only encodes the characters it
+        * absolutely needs to in order to make the resulting string a valid JCR
+        * name. Use {@link #unescapeIllegalJcrChars(String)} for decoding.
+        * <p>
+        * QName EBNF:<br>
+        * <xmp> simplename ::= onecharsimplename | twocharsimplename |
+        * threeormorecharname onecharsimplename ::= (* Any Unicode character
+        * except: '.', '/', ':', '[', ']', '*', '|' or any whitespace character *)
+        * twocharsimplename ::= '.' onecharsimplename | onecharsimplename '.' |
+        * onecharsimplename onecharsimplename threeormorecharname ::= nonspace
+        * string nonspace string ::= char | string char char ::= nonspace | ' '
+        * nonspace ::= (* Any Unicode character except: '/', ':', '[', ']', '*',
+        * '|' or any whitespace character *) </xmp>
+        *
+        * @param name
+        *            the name to escape
+        * @return the escaped name
+        */
+       public static String escapeIllegalJcrChars(String name) {
+               return escapeIllegalChars(name, "%/:[]*|\t\r\n");
+       }
+
+       /**
+        * Escapes all illegal JCR 1.0 name characters of a string. Use
+        * {@link #unescapeIllegalJcrChars(String)} for decoding.
+        * <p>
+        * QName EBNF:<br>
+        * <xmp> simplename ::= onecharsimplename | twocharsimplename |
+        * threeormorecharname onecharsimplename ::= (* Any Unicode character
+        * except: '.', '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace
+        * character *) twocharsimplename ::= '.' onecharsimplename |
+        * onecharsimplename '.' | onecharsimplename onecharsimplename
+        * threeormorecharname ::= nonspace string nonspace string ::= char | string
+        * char char ::= nonspace | ' ' nonspace ::= (* Any Unicode character
+        * except: '/', ':', '[', ']', '*', ''', '"', '|' or any whitespace
+        * character *) </xmp>
+        *
+        * @since Apache Jackrabbit 2.3.2 and 2.2.10
+        * @see <a href=
+        *      "https://issues.apache.org/jira/browse/JCR-3128">JCR-3128</a>
+        * @param name
+        *            the name to escape
+        * @return the escaped name
+        */
+       public static String escapeIllegalJcr10Chars(String name) {
+               return escapeIllegalChars(name, "%/:[]*'\"|\t\r\n");
+       }
+
+       private static String escapeIllegalChars(String name, String illegal) {
+               StringBuilder buffer = new StringBuilder(name.length() * 2);
+               for (int i = 0; i < name.length(); i++) {
+                       char ch = name.charAt(i);
+                       if (illegal.indexOf(ch) != -1 || (ch == '.' && name.length() < 3)
+                                       || (ch == ' ' && (i == 0 || i == name.length() - 1))) {
+                               buffer.append('%');
+                               buffer.append(Character.toUpperCase(Character.forDigit(ch / 16, 16)));
+                               buffer.append(Character.toUpperCase(Character.forDigit(ch % 16, 16)));
+                       } else {
+                               buffer.append(ch);
+                       }
+               }
+               return buffer.toString();
+       }
+
+       /**
+        * Escapes illegal XPath search characters at the end of a string.
+        * <p>
+        * Example:<br>
+        * A search string like 'test?' will run into a ParseException documented in
+        * http://issues.apache.org/jira/browse/JCR-1248
+        *
+        * @param s
+        *            the string to encode
+        * @return the escaped string
+        */
+       public static String escapeIllegalXpathSearchChars(String s) {
+               StringBuilder sb = new StringBuilder();
+               sb.append(s.substring(0, (s.length() - 1)));
+               char c = s.charAt(s.length() - 1);
+               // NOTE: keep this in sync with _ESCAPED_CHAR below!
+               if (c == '!' || c == '(' || c == ':' || c == '^' || c == '[' || c == ']' || c == '{' || c == '}' || c == '?') {
+                       sb.append('\\');
+               }
+               sb.append(c);
+               return sb.toString();
+       }
+
+       /**
+        * Unescapes previously escaped jcr chars.
+        * <p>
+        * Please note, that this does not exactly the same as the url related
+        * {@link #unescape(String)}, since it handles the byte-encoding
+        * differently.
+        *
+        * @param name
+        *            the name to unescape
+        * @return the unescaped name
+        */
+       public static String unescapeIllegalJcrChars(String name) {
+               StringBuilder buffer = new StringBuilder(name.length());
+               int i = name.indexOf('%');
+               while (i > -1 && i + 2 < name.length()) {
+                       buffer.append(name.toCharArray(), 0, i);
+                       int a = Character.digit(name.charAt(i + 1), 16);
+                       int b = Character.digit(name.charAt(i + 2), 16);
+                       if (a > -1 && b > -1) {
+                               buffer.append((char) (a * 16 + b));
+                               name = name.substring(i + 3);
+                       } else {
+                               buffer.append('%');
+                               name = name.substring(i + 1);
+                       }
+                       i = name.indexOf('%');
+               }
+               buffer.append(name);
+               return buffer.toString();
+       }
+
+       /**
+        * Returns the name part of the path. If the given path is already a name
+        * (i.e. contains no slashes) it is returned.
+        *
+        * @param path
+        *            the path
+        * @return the name part or <code>null</code> if <code>path</code> is
+        *         <code>null</code>.
+        */
+       public static String getName(String path) {
+               return getName(path, '/');
+       }
+
+       /**
+        * Returns the name part of the path, delimited by the given
+        * <code>delim</code>. If the given path is already a name (i.e. contains no
+        * <code>delim</code> characters) it is returned.
+        *
+        * @param path
+        *            the path
+        * @param delim
+        *            the delimiter
+        * @return the name part or <code>null</code> if <code>path</code> is
+        *         <code>null</code>.
+        */
+       public static String getName(String path, char delim) {
+               return path == null ? null : path.substring(path.lastIndexOf(delim) + 1);
+       }
+
+       /**
+        * Same as {@link #getName(String)} but adding the possibility to pass paths
+        * that end with a trailing '/'
+        *
+        * @see #getName(String)
+        */
+       public static String getName(String path, boolean ignoreTrailingSlash) {
+               if (ignoreTrailingSlash && path != null && path.endsWith("/") && path.length() > 1) {
+                       path = path.substring(0, path.length() - 1);
+               }
+               return getName(path);
+       }
+
+       /**
+        * Returns the namespace prefix of the given <code>qname</code>. If the
+        * prefix is missing, an empty string is returned. Please note, that this
+        * method does not validate the name or prefix.
+        * </p>
+        * the qname has the format: qname := [prefix ':'] local;
+        *
+        * @param qname
+        *            a qualified name
+        * @return the prefix of the name or "".
+        *
+        * @see #getLocalName(String)
+        *
+        * @throws NullPointerException
+        *             if <code>qname</code> is <code>null</code>
+        */
+       public static String getNamespacePrefix(String qname) {
+               int pos = qname.indexOf(':');
+               return pos >= 0 ? qname.substring(0, pos) : "";
+       }
+
+       /**
+        * Returns the local name of the given <code>qname</code>. Please note, that
+        * this method does not validate the name.
+        * </p>
+        * the qname has the format: qname := [prefix ':'] local;
+        *
+        * @param qname
+        *            a qualified name
+        * @return the localname
+        *
+        * @see #getNamespacePrefix(String)
+        *
+        * @throws NullPointerException
+        *             if <code>qname</code> is <code>null</code>
+        */
+       public static String getLocalName(String qname) {
+               int pos = qname.indexOf(':');
+               return pos >= 0 ? qname.substring(pos + 1) : qname;
+       }
+
+       /**
+        * Determines, if two paths denote hierarchical siblins.
+        *
+        * @param p1
+        *            first path
+        * @param p2
+        *            second path
+        * @return true if on same level, false otherwise
+        */
+       public static boolean isSibling(String p1, String p2) {
+               int pos1 = p1.lastIndexOf('/');
+               int pos2 = p2.lastIndexOf('/');
+               return (pos1 == pos2 && pos1 >= 0 && p1.regionMatches(0, p2, 0, pos1));
+       }
+
+       /**
+        * Determines if the <code>descendant</code> path is hierarchical a
+        * descendant of <code>path</code>.
+        *
+        * @param path
+        *            the current path
+        * @param descendant
+        *            the potential descendant
+        * @return <code>true</code> if the <code>descendant</code> is a descendant;
+        *         <code>false</code> otherwise.
+        */
+       public static boolean isDescendant(String path, String descendant) {
+               String pattern = path.endsWith("/") ? path : path + "/";
+               return !pattern.equals(descendant) && descendant.startsWith(pattern);
+       }
+
+       /**
+        * Determines if the <code>descendant</code> path is hierarchical a
+        * descendant of <code>path</code> or equal to it.
+        *
+        * @param path
+        *            the path to check
+        * @param descendant
+        *            the potential descendant
+        * @return <code>true</code> if the <code>descendant</code> is a descendant
+        *         or equal; <code>false</code> otherwise.
+        */
+       public static boolean isDescendantOrEqual(String path, String descendant) {
+               if (path.equals(descendant)) {
+                       return true;
+               } else {
+                       String pattern = path.endsWith("/") ? path : path + "/";
+                       return descendant.startsWith(pattern);
+               }
+       }
+
+       /**
+        * Returns the n<sup>th</sup> relative parent of the path, where n=level.
+        * <p>
+        * Example:<br>
+        * <code>
+        * Text.getRelativeParent("/foo/bar/test", 1) == "/foo/bar"
+        * </code>
+        *
+        * @param path
+        *            the path of the page
+        * @param level
+        *            the level of the parent
+        */
+       public static String getRelativeParent(String path, int level) {
+               int idx = path.length();
+               while (level > 0) {
+                       idx = path.lastIndexOf('/', idx - 1);
+                       if (idx < 0) {
+                               return "";
+                       }
+                       level--;
+               }
+               return (idx == 0) ? "/" : path.substring(0, idx);
+       }
+
+       /**
+        * Same as {@link #getRelativeParent(String, int)} but adding the
+        * possibility to pass paths that end with a trailing '/'
+        *
+        * @see #getRelativeParent(String, int)
+        */
+       public static String getRelativeParent(String path, int level, boolean ignoreTrailingSlash) {
+               if (ignoreTrailingSlash && path.endsWith("/") && path.length() > 1) {
+                       path = path.substring(0, path.length() - 1);
+               }
+               return getRelativeParent(path, level);
+       }
+
+       /**
+        * Returns the n<sup>th</sup> absolute parent of the path, where n=level.
+        * <p>
+        * Example:<br>
+        * <code>
+        * Text.getAbsoluteParent("/foo/bar/test", 1) == "/foo/bar"
+        * </code>
+        *
+        * @param path
+        *            the path of the page
+        * @param level
+        *            the level of the parent
+        */
+       public static String getAbsoluteParent(String path, int level) {
+               int idx = 0;
+               int len = path.length();
+               while (level >= 0 && idx < len) {
+                       idx = path.indexOf('/', idx + 1);
+                       if (idx < 0) {
+                               idx = len;
+                       }
+                       level--;
+               }
+               return level >= 0 ? "" : path.substring(0, idx);
+       }
+
+       /**
+        * Performs variable replacement on the given string value. Each
+        * <code>${...}</code> sequence within the given value is replaced with the
+        * value of the named parser variable. If a variable is not found in the
+        * properties an IllegalArgumentException is thrown unless
+        * <code>ignoreMissing</code> is <code>true</code>. In the later case, the
+        * missing variable is replaced by the empty string.
+        *
+        * @param value
+        *            the original value
+        * @param ignoreMissing
+        *            if <code>true</code>, missing variables are replaced by the
+        *            empty string.
+        * @return value after variable replacements
+        * @throws IllegalArgumentException
+        *             if the replacement of a referenced variable is not found
+        */
+       public static String replaceVariables(Properties variables, String value, boolean ignoreMissing)
+                       throws IllegalArgumentException {
+               StringBuilder result = new StringBuilder();
+
+               // Value:
+               // +--+-+--------+-+-----------------+
+               // | |p|--> |q|--> |
+               // +--+-+--------+-+-----------------+
+               int p = 0, q = value.indexOf("${"); // Find first ${
+               while (q != -1) {
+                       result.append(value.substring(p, q)); // Text before ${
+                       p = q;
+                       q = value.indexOf("}", q + 2); // Find }
+                       if (q != -1) {
+                               String variable = value.substring(p + 2, q);
+                               String replacement = variables.getProperty(variable);
+                               if (replacement == null) {
+                                       if (ignoreMissing) {
+                                               replacement = "";
+                                       } else {
+                                               throw new IllegalArgumentException("Replacement not found for ${" + variable + "}.");
+                                       }
+                               }
+                               result.append(replacement);
+                               p = q + 1;
+                               q = value.indexOf("${", p); // Find next ${
+                       }
+               }
+               result.append(value.substring(p, value.length())); // Trailing text
+
+               return result.toString();
+       }
+
+       private static byte decodeDigit(byte b) {
+               if (b >= 0x30 && b <= 0x39) {
+                       return (byte) (b - 0x30);
+               } else if (b >= 0x41 && b <= 0x46) {
+                       return (byte) (b - 0x37);
+               } else if (b >= 0x61 && b <= 0x66) {
+                       return (byte) (b - 0x57);
+               } else {
+                       throw new IllegalArgumentException("Escape sequence is not hexadecimal: " + (char) b);
+               }
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/fs/WorkSpaceFileStore.java b/org.argeo.jcr/src/org/argeo/jcr/fs/WorkSpaceFileStore.java
new file mode 100644 (file)
index 0000000..bdefff3
--- /dev/null
@@ -0,0 +1,67 @@
+package org.argeo.jcr.fs;
+
+import java.io.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileStoreAttributeView;
+
+import javax.jcr.Workspace;
+
+public class WorkSpaceFileStore extends FileStore {
+       private Workspace workspace;
+
+       public WorkSpaceFileStore(Workspace workspace) {
+               this.workspace = workspace;
+       }
+
+       @Override
+       public String name() {
+               return workspace.getName();
+       }
+
+       @Override
+       public String type() {
+               return "workspace";
+       }
+
+       @Override
+       public boolean isReadOnly() {
+               return false;
+       }
+
+       @Override
+       public long getTotalSpace() throws IOException {
+               return 0;
+       }
+
+       @Override
+       public long getUsableSpace() throws IOException {
+               return 0;
+       }
+
+       @Override
+       public long getUnallocatedSpace() throws IOException {
+               return 0;
+       }
+
+       @Override
+       public boolean supportsFileAttributeView(Class<? extends FileAttributeView> type) {
+               return false;
+       }
+
+       @Override
+       public boolean supportsFileAttributeView(String name) {
+               return false;
+       }
+
+       @Override
+       public <V extends FileStoreAttributeView> V getFileStoreAttributeView(Class<V> type) {
+               return null;
+       }
+
+       @Override
+       public Object getAttribute(String attribute) throws IOException {
+               return workspace.getSession().getRepository().getDescriptor(attribute);
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java b/org.argeo.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java
new file mode 100644 (file)
index 0000000..2699c54
--- /dev/null
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr.proxy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import javax.jcr.Binary;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.jcr.ArgeoJcrException;
+import org.argeo.jcr.JcrUtils;
+
+/** Base class for URL based proxys. */
+public abstract class AbstractUrlProxy implements ResourceProxy {
+       private final static Log log = LogFactory.getLog(AbstractUrlProxy.class);
+
+       private Repository jcrRepository;
+       private Session jcrAdminSession;
+       private String proxyWorkspace = "proxy";
+
+       protected abstract Node retrieve(Session session, String path);
+
+       void init() {
+               try {
+                       jcrAdminSession = JcrUtils.loginOrCreateWorkspace(jcrRepository, proxyWorkspace);
+                       beforeInitSessionSave(jcrAdminSession);
+                       if (jcrAdminSession.hasPendingChanges())
+                               jcrAdminSession.save();
+               } catch (Exception e) {
+                       JcrUtils.discardQuietly(jcrAdminSession);
+                       throw new ArgeoJcrException("Cannot initialize Maven proxy", e);
+               }
+       }
+
+       /**
+        * Called before the (admin) session is saved at the end of the initialization.
+        * Does nothing by default, to be overridden.
+        */
+       protected void beforeInitSessionSave(Session session) throws RepositoryException {
+       }
+
+       void destroy() {
+               JcrUtils.logoutQuietly(jcrAdminSession);
+       }
+
+       /**
+        * Called before the (admin) session is logged out when resources are released.
+        * Does nothing by default, to be overridden.
+        */
+       protected void beforeDestroySessionLogout() throws RepositoryException {
+       }
+
+       public Node proxy(String path) {
+               // we open a JCR session with client credentials in order not to use the
+               // admin session in multiple thread or make it a bottleneck.
+               Node nodeAdmin = null;
+               Node nodeClient = null;
+               Session clientSession = null;
+               try {
+                       clientSession = jcrRepository.login(proxyWorkspace);
+                       if (!clientSession.itemExists(path) || shouldUpdate(clientSession, path)) {
+                               nodeAdmin = retrieveAndSave(path);
+                               if (nodeAdmin != null)
+                                       nodeClient = clientSession.getNode(path);
+                       } else
+                               nodeClient = clientSession.getNode(path);
+                       return nodeClient;
+               } catch (RepositoryException e) {
+                       throw new ArgeoJcrException("Cannot proxy " + path, e);
+               } finally {
+                       if (nodeClient == null)
+                               JcrUtils.logoutQuietly(clientSession);
+               }
+       }
+
+       protected synchronized Node retrieveAndSave(String path) {
+               try {
+                       Node node = retrieve(jcrAdminSession, path);
+                       if (node == null)
+                               return null;
+                       jcrAdminSession.save();
+                       return node;
+               } catch (RepositoryException e) {
+                       JcrUtils.discardQuietly(jcrAdminSession);
+                       throw new ArgeoJcrException("Cannot retrieve and save " + path, e);
+               } finally {
+                       notifyAll();
+               }
+       }
+
+       /** Session is not saved */
+       protected synchronized Node proxyUrl(Session session, String remoteUrl, String path) throws RepositoryException {
+               Node node = null;
+               if (session.itemExists(path)) {
+                       // throw new ArgeoJcrException("Node " + path + " already exists");
+               }
+               try (InputStream in = new URL(remoteUrl).openStream()) {
+                       // URL u = new URL(remoteUrl);
+                       // in = u.openStream();
+                       node = importFile(session, path, in);
+               } catch (IOException e) {
+                       if (log.isDebugEnabled()) {
+                               log.debug("Cannot read " + remoteUrl + ", skipping... " + e.getMessage());
+                               // log.trace("Cannot read because of ", e);
+                       }
+                       JcrUtils.discardQuietly(session);
+                       // } finally {
+                       // IOUtils.closeQuietly(in);
+               }
+               return node;
+       }
+
+       protected synchronized Node importFile(Session session, String path, InputStream in) throws RepositoryException {
+               Binary binary = null;
+               try {
+                       Node content = null;
+                       Node node = null;
+                       if (!session.itemExists(path)) {
+                               node = JcrUtils.mkdirs(session, path, NodeType.NT_FILE, NodeType.NT_FOLDER, false);
+                               content = node.addNode(Node.JCR_CONTENT, NodeType.NT_RESOURCE);
+                       } else {
+                               node = session.getNode(path);
+                               content = node.getNode(Node.JCR_CONTENT);
+                       }
+                       binary = session.getValueFactory().createBinary(in);
+                       content.setProperty(Property.JCR_DATA, binary);
+                       JcrUtils.updateLastModifiedAndParents(node, null);
+                       return node;
+               } finally {
+                       JcrUtils.closeQuietly(binary);
+               }
+       }
+
+       /** Whether the file should be updated. */
+       protected Boolean shouldUpdate(Session clientSession, String nodePath) {
+               return false;
+       }
+
+       public void setJcrRepository(Repository jcrRepository) {
+               this.jcrRepository = jcrRepository;
+       }
+
+       public void setProxyWorkspace(String localWorkspace) {
+               this.proxyWorkspace = localWorkspace;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java b/org.argeo.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java
new file mode 100644 (file)
index 0000000..b4fb332
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr.proxy;
+
+import javax.jcr.Node;
+
+/** A proxy which nows how to resolve and synchronize relative URLs */
+public interface ResourceProxy {
+       /**
+        * Proxy the file referenced by this relative path in the underlying
+        * repository. A new session is created by each call, so the underlying
+        * session of the returned node must be closed by the caller.
+        * 
+        * @return the proxied Node, <code>null</code> if the resource was not found
+        *         (e.g. HTTP 404)
+        */
+       public Node proxy(String relativePath);
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java b/org.argeo.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java
new file mode 100644 (file)
index 0000000..c29b13a
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr.proxy;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.RepositoryException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.jcr.ArgeoJcrException;
+import org.argeo.jcr.Bin;
+import org.argeo.jcr.JcrUtils;
+
+/** Wraps a proxy via HTTP */
+public class ResourceProxyServlet extends HttpServlet {
+       private static final long serialVersionUID = -8886549549223155801L;
+
+       private final static Log log = LogFactory
+                       .getLog(ResourceProxyServlet.class);
+
+       private ResourceProxy proxy;
+
+       private String contentTypeCharset = "UTF-8";
+
+       @Override
+       protected void doGet(HttpServletRequest request,
+                       HttpServletResponse response) throws ServletException, IOException {
+               String path = request.getPathInfo();
+
+               if (log.isTraceEnabled()) {
+                       log.trace("path=" + path);
+                       log.trace("UserPrincipal = " + request.getUserPrincipal().getName());
+                       log.trace("SessionID = " + request.getSession(false).getId());
+                       log.trace("ContextPath = " + request.getContextPath());
+                       log.trace("ServletPath = " + request.getServletPath());
+                       log.trace("PathInfo = " + request.getPathInfo());
+                       log.trace("Method = " + request.getMethod());
+                       log.trace("User-Agent = " + request.getHeader("User-Agent"));
+               }
+
+               Node node = null;
+               try {
+                       node = proxy.proxy(path);
+                       if (node == null)
+                               response.sendError(404);
+                       else
+                               processResponse(node, response);
+               } finally {
+                       if (node != null)
+                               try {
+                                       JcrUtils.logoutQuietly(node.getSession());
+                               } catch (RepositoryException e) {
+                                       // silent
+                               }
+               }
+
+       }
+
+       /** Retrieve the content of the node. */
+       protected void processResponse(Node node, HttpServletResponse response) {
+//             Binary binary = null;
+//             InputStream in = null;
+               try(Bin binary = new Bin( node.getNode(Property.JCR_CONTENT)
+                               .getProperty(Property.JCR_DATA));InputStream in = binary.getStream()) {
+                       String fileName = node.getName();
+                       String ext = FilenameUtils.getExtension(fileName);
+
+                       // TODO use a more generic / standard approach
+                       // see http://svn.apache.org/viewvc/tomcat/trunk/conf/web.xml
+                       String contentType;
+                       if ("xml".equals(ext))
+                               contentType = "text/xml;charset=" + contentTypeCharset;
+                       else if ("jar".equals(ext))
+                               contentType = "application/java-archive";
+                       else if ("zip".equals(ext))
+                               contentType = "application/zip";
+                       else if ("gz".equals(ext))
+                               contentType = "application/x-gzip";
+                       else if ("bz2".equals(ext))
+                               contentType = "application/x-bzip2";
+                       else if ("tar".equals(ext))
+                               contentType = "application/x-tar";
+                       else if ("rpm".equals(ext))
+                               contentType = "application/x-redhat-package-manager";
+                       else
+                               contentType = "application/octet-stream";
+                       contentType = contentType + ";name=\"" + fileName + "\"";
+                       response.setHeader("Content-Disposition", "attachment; filename=\""
+                                       + fileName + "\"");
+                       response.setHeader("Expires", "0");
+                       response.setHeader("Cache-Control", "no-cache, must-revalidate");
+                       response.setHeader("Pragma", "no-cache");
+
+                       response.setContentType(contentType);
+
+//                     try {
+//                             binary = node.getNode(Property.JCR_CONTENT)
+//                                             .getProperty(Property.JCR_DATA).getBinary();
+//                     } catch (PathNotFoundException e) {
+//                             log.error("Node " + node + " as no data under content");
+//                             throw e;
+//                     }
+//                     in = binary.getStream();
+                       IOUtils.copy(in, response.getOutputStream());
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot download " + node, e);
+//             } finally {
+//                     IOUtils.closeQuietly(in);
+//                     JcrUtils.closeQuietly(binary);
+               }
+       }
+
+       public void setProxy(ResourceProxy resourceProxy) {
+               this.proxy = resourceProxy;
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/spring/ThreadBoundSession.java b/org.argeo.jcr/src/org/argeo/jcr/spring/ThreadBoundSession.java
new file mode 100644 (file)
index 0000000..35f0215
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr.spring;
+
+import org.argeo.jcr.ThreadBoundJcrSessionFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.InitializingBean;
+
+@SuppressWarnings("rawtypes")
+@Deprecated
+public class ThreadBoundSession extends ThreadBoundJcrSessionFactory implements FactoryBean, InitializingBean, DisposableBean{
+       public void afterPropertiesSet() throws Exception {
+               init();
+       }
+
+       public void destroy() throws Exception {
+               dispose();
+       }
+
+}
diff --git a/org.argeo.jcr/src/org/argeo/jcr/unit/AbstractJcrTestCase.java b/org.argeo.jcr/src/org/argeo/jcr/unit/AbstractJcrTestCase.java
new file mode 100644 (file)
index 0000000..1269a3e
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.jcr.unit;
+
+import java.io.File;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+
+import javax.jcr.Repository;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.argeo.jcr.ArgeoJcrException;
+
+import junit.framework.TestCase;
+
+public abstract class AbstractJcrTestCase extends TestCase {
+       private final static Log log = LogFactory.getLog(AbstractJcrTestCase.class);
+
+       private Repository repository;
+       private Session session = null;
+
+       public final static String LOGIN_CONTEXT_TEST_SYSTEM = "TEST_JACKRABBIT_ADMIN";
+
+       // protected abstract File getRepositoryFile() throws Exception;
+
+       protected abstract Repository createRepository() throws Exception;
+
+       protected abstract void clearRepository(Repository repository)
+                       throws Exception;
+
+       @Override
+       protected void setUp() throws Exception {
+               File homeDir = getHomeDir();
+               FileUtils.deleteDirectory(homeDir);
+               repository = createRepository();
+       }
+
+       @Override
+       protected void tearDown() throws Exception {
+               if (session != null) {
+                       session.logout();
+                       if (log.isTraceEnabled())
+                               log.trace("Logout session");
+               }
+               clearRepository(repository);
+       }
+
+       protected Session session() {
+               if (session != null && session.isLive())
+                       return session;
+               Session session;
+               if (getLoginContext() != null) {
+                       LoginContext lc;
+                       try {
+                               lc = new LoginContext(getLoginContext());
+                               lc.login();
+                       } catch (LoginException e) {
+                               throw new ArgeoJcrException("JAAS login failed", e);
+                       }
+                       session = Subject.doAs(lc.getSubject(),
+                                       new PrivilegedAction<Session>() {
+
+                                               @Override
+                                               public Session run() {
+                                                       return login();
+                                               }
+
+                                       });
+               } else
+                       session = login();
+               this.session = session;
+               return this.session;
+       }
+
+       protected String getLoginContext() {
+               return null;
+       }
+
+       protected Session login() {
+               try {
+                       if (log.isTraceEnabled())
+                               log.trace("Login session");
+                       Subject subject = Subject.getSubject(AccessController.getContext());
+                       if (subject != null)
+                               return getRepository().login();
+                       else
+                               return getRepository().login(
+                                               new SimpleCredentials("demo", "demo".toCharArray()));
+               } catch (Exception e) {
+                       throw new ArgeoJcrException("Cannot login to repository", e);
+               }
+       }
+
+       protected Repository getRepository() {
+               return repository;
+       }
+
+       /**
+        * enables children class to set an existing repository in case it is not
+        * deleted on startup, to test migration by instance
+        */
+       public void setRepository(Repository repository) {
+               this.repository = repository;
+       }
+
+       protected File getHomeDir() {
+               File homeDir = new File(System.getProperty("java.io.tmpdir"),
+                               AbstractJcrTestCase.class.getSimpleName() + "-"
+                                               + System.getProperty("user.name"));
+               return homeDir;
+       }
+
+}
diff --git a/org.argeo.maintenance/.classpath b/org.argeo.maintenance/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.maintenance/.gitignore b/org.argeo.maintenance/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.maintenance/.project b/org.argeo.maintenance/.project
new file mode 100644 (file)
index 0000000..d1c87c7
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.maintenance</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.maintenance/META-INF/.gitignore b/org.argeo.maintenance/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.maintenance/bnd.bnd b/org.argeo.maintenance/bnd.bnd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/org.argeo.maintenance/build.properties b/org.argeo.maintenance/build.properties
new file mode 100644 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
diff --git a/org.argeo.maintenance/pom.xml b/org.argeo.maintenance/pom.xml
new file mode 100644 (file)
index 0000000..c93cd01
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.maintenance</artifactId>
+       <name>Maintenance</name>
+       <packaging>jar</packaging>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.jcr</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.enterprise</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/MaintenanceException.java b/org.argeo.maintenance/src/org/argeo/maintenance/MaintenanceException.java
new file mode 100644 (file)
index 0000000..dc4243e
--- /dev/null
@@ -0,0 +1,13 @@
+package org.argeo.maintenance;
+
+public class MaintenanceException extends RuntimeException {
+       private static final long serialVersionUID = -4571088120514827735L;
+
+       public MaintenanceException(String message) {
+               super(message);
+       }
+
+       public MaintenanceException(String message, Throwable cause) {
+               super(message, cause);
+       }
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/AbstractAtomicBackup.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/AbstractAtomicBackup.java
new file mode 100644 (file)
index 0000000..ae587ea
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.provider.sftp.SftpFileSystemConfigBuilder;
+import org.argeo.maintenance.MaintenanceException;
+
+/**
+ * Simplify atomic backups implementation, especially by managing VFS.
+ */
+public abstract class AbstractAtomicBackup implements AtomicBackup {
+       private String name;
+       private String compression = "bz2";
+
+       protected abstract void writeBackup(FileObject targetFo);
+
+       public AbstractAtomicBackup() {
+       }
+
+       public AbstractAtomicBackup(String name) {
+               this.name = name;
+       }
+
+       public void init() {
+               if (name == null)
+                       throw new MaintenanceException("Atomic backup name must be set");
+       }
+
+       public void destroy() {
+
+       }
+
+       @Override
+       public String backup(FileSystemManager fileSystemManager,
+                       String backupsBase, BackupContext backupContext,
+                       FileSystemOptions opts) {
+               if (name == null)
+                       throw new MaintenanceException("Atomic backup name must be set");
+
+               FileObject targetFo = null;
+               try {
+                       if (backupsBase.startsWith("sftp:"))
+                               SftpFileSystemConfigBuilder.getInstance()
+                                               .setStrictHostKeyChecking(opts, "no");
+                       if (compression == null || compression.equals("none"))
+                               targetFo = fileSystemManager.resolveFile(backupsBase + '/'
+                                               + backupContext.getRelativeFolder() + '/' + name, opts);
+                       else if (compression.equals("bz2"))
+                               targetFo = fileSystemManager.resolveFile("bz2:" + backupsBase
+                                               + '/' + backupContext.getRelativeFolder() + '/' + name
+                                               + ".bz2" + "!" + name, opts);
+                       else if (compression.equals("gz"))
+                               targetFo = fileSystemManager.resolveFile("gz:" + backupsBase
+                                               + '/' + backupContext.getRelativeFolder() + '/' + name
+                                               + ".gz" + "!" + name, opts);
+                       else
+                               throw new MaintenanceException("Unsupported compression "
+                                               + compression);
+
+                       writeBackup(targetFo);
+
+                       return targetFo.toString();
+               } catch (Exception e) {
+                       throw new MaintenanceException("Cannot backup " + name + " to "
+                                       + targetFo, e);
+               } finally {
+                       BackupUtils.closeFOQuietly(targetFo);
+               }
+       }
+
+       public void setName(String name) {
+               this.name = name;
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public void setCompression(String compression) {
+               this.compression = compression;
+       }
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/AtomicBackup.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/AtomicBackup.java
new file mode 100644 (file)
index 0000000..e8c3ded
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+
+/** Performs the backup of a single component, typically a database dump */
+public interface AtomicBackup {
+       /** Name identifiying this backup */
+       public String getName();
+
+       /**
+        * Retrieves the data of the component in a format that allows to restore
+        * the component
+        * 
+        * @param backupContext
+        *            the context of this backup
+        * @return the VFS URI of the generated file or directory
+        */
+       public String backup(FileSystemManager fileSystemManager,
+                       String backupsBase, BackupContext backupContext,
+                       FileSystemOptions opts);
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupContext.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupContext.java
new file mode 100644 (file)
index 0000000..cc1391f
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+/**
+ * Transient information of a given backup, centralizing common information such
+ * as timestamp and location.
+ */
+public interface BackupContext {
+       /** Backup date */
+       public Date getTimestamp();
+
+       /** Formatted backup date */
+       public String getTimestampAsString();
+
+       /** System name */
+       public String getSystemName();
+
+       /** Local base */
+       public String getRelativeFolder();
+
+       /** Date format */
+       public DateFormat getDateFormat();
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupFileSystemManager.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupFileSystemManager.java
new file mode 100644 (file)
index 0000000..3053f0a
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
+import org.apache.commons.vfs2.provider.bzip2.Bzip2FileProvider;
+import org.apache.commons.vfs2.provider.ftp.FtpFileProvider;
+import org.apache.commons.vfs2.provider.gzip.GzipFileProvider;
+import org.apache.commons.vfs2.provider.local.DefaultLocalFileProvider;
+import org.apache.commons.vfs2.provider.ram.RamFileProvider;
+import org.apache.commons.vfs2.provider.sftp.SftpFileProvider;
+import org.apache.commons.vfs2.provider.url.UrlFileProvider;
+import org.argeo.maintenance.MaintenanceException;
+
+/**
+ * Programatically configured VFS file system manager which can be declared as a
+ * bean and associated with a life cycle (methods
+ * {@link DefaultFileSystemManager#init()} and
+ * {@link DefaultFileSystemManager#close()}). Supports bz2, file, ram, gzip,
+ * ftp, sftp
+ */
+public class BackupFileSystemManager extends DefaultFileSystemManager {
+
+       public BackupFileSystemManager() {
+               super();
+               try {
+                       addProvider("file", new DefaultLocalFileProvider());
+                       addProvider("bz2", new Bzip2FileProvider());
+                       addProvider("ftp", new FtpFileProvider());
+                       addProvider("sftp", new SftpFileProvider());
+                       addProvider("gzip", new GzipFileProvider());
+                       addProvider("ram", new RamFileProvider());
+                       setDefaultProvider(new UrlFileProvider());
+               } catch (FileSystemException e) {
+                       throw new MaintenanceException("Cannot configure backup file provider", e);
+               }
+       }
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupPurge.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupPurge.java
new file mode 100644 (file)
index 0000000..face8a1
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import java.text.DateFormat;
+
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+
+/** Purges previous backups */
+public interface BackupPurge {
+       /**
+        * Purge the backups identified by these arguments. Although these are the
+        * same fields as a {@link BackupContext} we don't pass it as argument since
+        * we want to use this interface to purge remote backups as well (that is,
+        * with a different base), or outside the scope of a running backup.
+        */
+       public void purge(FileSystemManager fileSystemManager, String base,
+                       String name, DateFormat dateFormat, FileSystemOptions opts);
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupUtils.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/BackupUtils.java
new file mode 100644 (file)
index 0000000..94e8a1d
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileObject;
+
+/** Backup utilities */
+public class BackupUtils {
+       /** Close a file object quietly even if it is null or throws an exception. */
+       public static void closeFOQuietly(FileObject fo) {
+               if (fo != null) {
+                       try {
+                               fo.close();
+                       } catch (Exception e) {
+                               // silent
+                       }
+               }
+       }
+       
+       /** Prevents instantiation */
+       private BackupUtils() {
+       }
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/MySqlBackup.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/MySqlBackup.java
new file mode 100644 (file)
index 0000000..7ab2d6a
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileObject;
+
+/** Backups a MySQL database using mysqldump. */
+public class MySqlBackup extends OsCallBackup {
+       private String mysqldumpLocation = "/usr/bin/mysqldump";
+
+       private String dbUser;
+       private String dbPassword;
+       private String dbName;
+
+       public MySqlBackup() {
+       }
+
+       public MySqlBackup(String dbUser, String dbPassword, String dbName) {
+               this.dbUser = dbUser;
+               this.dbPassword = dbPassword;
+               this.dbName = dbName;
+               init();
+       }
+
+       @Override
+       public void init() {
+               if (getName() == null)
+                       setName(dbName + ".mysql");
+               super.init();
+       }
+
+       @Override
+       public void writeBackup(FileObject targetFo) {
+               if (getCommand() == null)
+                       setCommand(mysqldumpLocation
+                                       + " --lock-tables --add-locks --add-drop-table"
+                                       + " -u ${dbUser} --password=${dbPassword} --databases ${dbName}");
+               getVariables().put("dbUser", dbUser);
+               getVariables().put("dbPassword", dbPassword);
+               getVariables().put("dbName", dbName);
+
+               super.writeBackup(targetFo);
+       }
+
+       public void setDbUser(String dbUser) {
+               this.dbUser = dbUser;
+       }
+
+       public void setDbPassword(String dbPassword) {
+               this.dbPassword = dbPassword;
+       }
+
+       public void setDbName(String dbName) {
+               this.dbName = dbName;
+       }
+
+       public void setMysqldumpLocation(String mysqldumpLocation) {
+               this.mysqldumpLocation = mysqldumpLocation;
+       }
+
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/OpenLdapBackup.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/OpenLdapBackup.java
new file mode 100644 (file)
index 0000000..415bc24
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileObject;
+import org.argeo.maintenance.MaintenanceException;
+
+/** Backups an OpenLDAP server using slapcat */
+public class OpenLdapBackup extends OsCallBackup {
+       private String slapcatLocation = "/usr/sbin/slapcat";
+       private String slapdConfLocation = "/etc/openldap/slapd.conf";
+       private String baseDn;
+
+       public OpenLdapBackup() {
+               super();
+       }
+
+       public OpenLdapBackup(String baseDn) {
+               super();
+               this.baseDn = baseDn;
+       }
+
+       @Override
+       public void writeBackup(FileObject targetFo) {
+               if (baseDn == null)
+                       throw new MaintenanceException("Base DN must be set");
+
+               if (getCommand() == null)
+                       setCommand(slapcatLocation
+                                       + " -f ${slapdConfLocation} -b '${baseDn}'");
+               getVariables().put("slapdConfLocation", slapdConfLocation);
+               getVariables().put("baseDn", baseDn);
+
+               super.writeBackup(targetFo);
+       }
+
+       public void setSlapcatLocation(String slapcatLocation) {
+               this.slapcatLocation = slapcatLocation;
+       }
+
+       public void setSlapdConfLocation(String slapdConfLocation) {
+               this.slapdConfLocation = slapdConfLocation;
+       }
+
+       public void setBaseDn(String baseDn) {
+               this.baseDn = baseDn;
+       }
+
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/OsCallBackup.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/OsCallBackup.java
new file mode 100644 (file)
index 0000000..07589d3
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import java.io.ByteArrayOutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.exec.CommandLine;
+import org.apache.commons.exec.DefaultExecutor;
+import org.apache.commons.exec.ExecuteException;
+import org.apache.commons.exec.ExecuteStreamHandler;
+import org.apache.commons.exec.Executor;
+import org.apache.commons.exec.PumpStreamHandler;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs2.FileContent;
+import org.apache.commons.vfs2.FileObject;
+import org.argeo.maintenance.MaintenanceException;
+
+/**
+ * Runs an OS command and save its standard output as a file. Typically used for
+ * MySQL or OpenLDAP dumps.
+ */
+public class OsCallBackup extends AbstractAtomicBackup {
+       private final static Log log = LogFactory.getLog(OsCallBackup.class);
+
+       private String command;
+       private Map<String, String> variables = new HashMap<String, String>();
+       private Executor executor = new DefaultExecutor();
+
+       private Map<String, String> environment = new HashMap<String, String>();
+
+       /** Name of the sudo user, root if "", not sudo if null */
+       private String sudo = null;
+
+       public OsCallBackup() {
+       }
+
+       public OsCallBackup(String name) {
+               super(name);
+       }
+
+       public OsCallBackup(String name, String command) {
+               super(name);
+               this.command = command;
+       }
+
+       @Override
+       public void writeBackup(FileObject targetFo) {
+               String commandToUse = command;
+
+               // sudo
+               if (sudo != null) {
+                       if (sudo.equals(""))
+                               commandToUse = "sudo " + commandToUse;
+                       else
+                               commandToUse = "sudo -u " + sudo + " " + commandToUse;
+               }
+
+               CommandLine commandLine = CommandLine.parse(commandToUse, variables);
+               ByteArrayOutputStream errBos = new ByteArrayOutputStream();
+               if (log.isTraceEnabled())
+                       log.trace(commandLine.toString());
+
+               try {
+                       // stdout
+                       FileContent targetContent = targetFo.getContent();
+                       // stderr
+                       ExecuteStreamHandler streamHandler = new PumpStreamHandler(targetContent.getOutputStream(), errBos);
+                       executor.setStreamHandler(streamHandler);
+                       executor.execute(commandLine, environment);
+               } catch (ExecuteException e) {
+                       byte[] err = errBos.toByteArray();
+                       String errStr = new String(err);
+                       throw new MaintenanceException("Process " + commandLine + " failed (" + e.getExitValue() + "): " + errStr, e);
+               } catch (Exception e) {
+                       byte[] err = errBos.toByteArray();
+                       String errStr = new String(err);
+                       throw new MaintenanceException("Process " + commandLine + " failed: " + errStr, e);
+               } finally {
+                       IOUtils.closeQuietly(errBos);
+               }
+       }
+
+       public void setCommand(String command) {
+               this.command = command;
+       }
+
+       protected String getCommand() {
+               return command;
+       }
+
+       /**
+        * A reference to the environment variables that will be passed to the
+        * process. Empty by default.
+        */
+       protected Map<String, String> getEnvironment() {
+               return environment;
+       }
+
+       protected Map<String, String> getVariables() {
+               return variables;
+       }
+
+       public void setVariables(Map<String, String> variables) {
+               this.variables = variables;
+       }
+
+       public void setExecutor(Executor executor) {
+               this.executor = executor;
+       }
+
+       public void setSudo(String sudo) {
+               this.sudo = sudo;
+       }
+
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/PostgreSqlBackup.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/PostgreSqlBackup.java
new file mode 100644 (file)
index 0000000..da3ea38
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import org.apache.commons.vfs2.FileObject;
+
+/** Backups a PostgreSQL database using pg_dump. */
+public class PostgreSqlBackup extends OsCallBackup {
+       /**
+        * PostgreSQL password environment variable (see
+        * http://stackoverflow.com/questions
+        * /2893954/how-to-pass-in-password-to-pg-dump)
+        */
+       protected final static String PGPASSWORD = "PGPASSWORD";
+
+       private String pgDumpLocation = "/usr/bin/pg_dump";
+
+       private String dbUser;
+       private String dbPassword;
+       private String dbName;
+
+       public PostgreSqlBackup() {
+               super();
+       }
+
+       public PostgreSqlBackup(String dbUser, String dbPassword, String dbName) {
+               this.dbUser = dbUser;
+               this.dbPassword = dbPassword;
+               this.dbName = dbName;
+               init();
+       }
+
+       @Override
+       public void init() {
+               // disable compression since pg_dump is used with -Fc option
+               setCompression(null);
+
+               if (getName() == null)
+                       setName(dbName + ".pgdump");
+               super.init();
+       }
+
+       @Override
+       public void writeBackup(FileObject targetFo) {
+               if (getCommand() == null) {
+                       getEnvironment().put(PGPASSWORD, dbPassword);
+                       setCommand(pgDumpLocation + " -Fc" + " -U ${dbUser} ${dbName}");
+               }
+               getVariables().put("dbUser", dbUser);
+               getVariables().put("dbPassword", dbPassword);
+               getVariables().put("dbName", dbName);
+
+               super.writeBackup(targetFo);
+       }
+
+       public void setDbUser(String dbUser) {
+               this.dbUser = dbUser;
+       }
+
+       public void setDbPassword(String dbPassword) {
+               this.dbPassword = dbPassword;
+       }
+
+       public void setDbName(String dbName) {
+               this.dbName = dbName;
+       }
+
+       public void setPgDumpLocation(String mysqldumpLocation) {
+               this.pgDumpLocation = mysqldumpLocation;
+       }
+
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SimpleBackupContext.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SimpleBackupContext.java
new file mode 100644 (file)
index 0000000..bef6f65
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import org.apache.commons.vfs2.FileSystemManager;
+
+/** Simple implementation of a backup context */
+public class SimpleBackupContext implements BackupContext {
+       private DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmm");
+       private final Date timestamp;
+       private final String name;
+
+       private final FileSystemManager fileSystemManager;
+
+       public SimpleBackupContext(FileSystemManager fileSystemManager,
+                       String backupsBase, String name) {
+               this.name = name;
+               this.timestamp = new Date();
+               this.fileSystemManager = fileSystemManager;
+       }
+
+       public Date getTimestamp() {
+               return timestamp;
+       }
+
+       public String getTimestampAsString() {
+               return dateFormat.format(timestamp);
+       }
+
+       public String getSystemName() {
+               return name;
+       }
+
+       public String getRelativeFolder() {
+               return name + '/' + getTimestampAsString();
+       }
+
+       public DateFormat getDateFormat() {
+               return dateFormat;
+       }
+
+       public FileSystemManager getFileSystemManager() {
+               return fileSystemManager;
+       }
+
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SimpleBackupPurge.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SimpleBackupPurge.java
new file mode 100644 (file)
index 0000000..042070f
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import java.text.DateFormat;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.Selectors;
+import org.argeo.maintenance.MaintenanceException;
+
+/** Simple backup purge which keeps backups only for a given number of days */
+public class SimpleBackupPurge implements BackupPurge {
+       private final static Log log = LogFactory.getLog(SimpleBackupPurge.class);
+
+       private Integer daysKept = 30;
+
+       @Override
+       public void purge(FileSystemManager fileSystemManager, String base, String name, DateFormat dateFormat,
+                       FileSystemOptions opts) {
+               try {
+                       ZonedDateTime nowDt = ZonedDateTime.now();
+                       FileObject baseFo = fileSystemManager.resolveFile(base + '/' + name, opts);
+
+                       SortedMap<ZonedDateTime, FileObject> toDelete = new TreeMap<ZonedDateTime, FileObject>();
+                       int backupCount = 0;
+
+                       // make sure base dir exists
+                       baseFo.createFolder();
+
+                       // scan backups and list those which should be deleted
+                       for (FileObject backupFo : baseFo.getChildren()) {
+                               String backupName = backupFo.getName().getBaseName();
+                               Date backupDate = dateFormat.parse(backupName);
+                               backupCount++;
+                               ZonedDateTime backupDt = ZonedDateTime.ofInstant(backupDate.toInstant(), ZoneId.systemDefault());
+                               Period sinceThen = Period.between(backupDt.toLocalDate(), nowDt.toLocalDate());
+                               // new Period(backupDt, nowDt);
+                               int days = sinceThen.getDays();
+                               // int days = sinceThen.getMinutes();
+                               if (days > daysKept) {
+                                       toDelete.put(backupDt, backupFo);
+                               }
+                       }
+
+                       if (toDelete.size() != 0 && toDelete.size() == backupCount) {
+                               // all backups would be deleted
+                               // but we want to keep at least one
+                               ZonedDateTime lastBackupDt = toDelete.firstKey();
+                               FileObject keptFo = toDelete.remove(lastBackupDt);
+                               log.warn("Backup " + keptFo + " kept although it is older than " + daysKept + " days.");
+                       }
+
+                       // delete old backups
+                       for (FileObject backupFo : toDelete.values()) {
+                               backupFo.delete(Selectors.SELECT_ALL);
+                               if (log.isDebugEnabled())
+                                       log.debug("Deleted backup " + backupFo);
+                       }
+               } catch (Exception e) {
+                       throw new MaintenanceException("Could not purge previous backups", e);
+               }
+
+       }
+
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SvnBackup.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SvnBackup.java
new file mode 100644 (file)
index 0000000..b725a3e
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import java.io.File;
+
+import org.apache.commons.vfs2.FileObject;
+
+/** Backups a Subversion repository using svnadmin. */
+public class SvnBackup extends OsCallBackup {
+       private String svnadminLocation = "/usr/bin/svnadmin";
+
+       private String repoLocation;
+       private String repoName;
+
+       public SvnBackup() {
+       }
+
+       public SvnBackup(String repoLocation) {
+               this.repoLocation = repoLocation;
+               init();
+       }
+
+       @Override
+       public void init() {
+               // use directory as repo name
+               if (repoName == null)
+                       repoName = new File(repoLocation).getName();
+
+               if (getName() == null)
+                       setName(repoName + ".svndump");
+               super.init();
+       }
+
+       @Override
+       public void writeBackup(FileObject targetFo) {
+               if (getCommand() == null) {
+                       setCommand(svnadminLocation + " dump " + " ${repoLocation}");
+               }
+               getVariables().put("repoLocation", repoLocation);
+
+               super.writeBackup(targetFo);
+       }
+
+       public void setRepoLocation(String repoLocation) {
+               this.repoLocation = repoLocation;
+       }
+
+       public void setRepoName(String repoName) {
+               this.repoName = repoName;
+       }
+
+       public void setSvnadminLocation(String mysqldumpLocation) {
+               this.svnadminLocation = mysqldumpLocation;
+       }
+
+}
diff --git a/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SystemBackup.java b/org.argeo.maintenance/src/org/argeo/maintenance/backup/vfs/SystemBackup.java
new file mode 100644 (file)
index 0000000..480f6dc
--- /dev/null
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.maintenance.backup.vfs;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemManager;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.Selectors;
+import org.apache.commons.vfs2.UserAuthenticator;
+import org.apache.commons.vfs2.impl.DefaultFileSystemConfigBuilder;
+import org.argeo.maintenance.MaintenanceException;
+import org.argeo.util.LangUtils;
+
+/**
+ * Combines multiple backups and transfer them to a remote location. Purges
+ * remote and local data based on certain criteria.
+ */
+public class SystemBackup implements Runnable {
+       private final static Log log = LogFactory.getLog(SystemBackup.class);
+
+       private FileSystemManager fileSystemManager;
+       private UserAuthenticator userAuthenticator = null;
+
+       private String backupsBase;
+       private String systemName;
+
+       private List<AtomicBackup> atomicBackups = new ArrayList<AtomicBackup>();
+       private BackupPurge backupPurge = new SimpleBackupPurge();
+
+       private Map<String, UserAuthenticator> remoteBases = new HashMap<String, UserAuthenticator>();
+
+       @Override
+       public void run() {
+               if (atomicBackups.size() == 0)
+                       throw new MaintenanceException("No atomic backup listed");
+               List<String> failures = new ArrayList<String>();
+
+               SimpleBackupContext backupContext = new SimpleBackupContext(fileSystemManager, backupsBase, systemName);
+
+               // purge older backups
+               FileSystemOptions opts = new FileSystemOptions();
+               try {
+                       DefaultFileSystemConfigBuilder.getInstance().setUserAuthenticator(opts, userAuthenticator);
+               } catch (FileSystemException e) {
+                       throw new MaintenanceException("Cannot create authentication", e);
+               }
+
+               try {
+
+                       backupPurge.purge(fileSystemManager, backupsBase, systemName, backupContext.getDateFormat(), opts);
+               } catch (Exception e) {
+                       failures.add("Purge " + backupsBase + " failed: " + e.getMessage());
+                       log.error("Purge of " + backupsBase + " failed", e);
+               }
+
+               // perform backup
+               for (AtomicBackup atomickBackup : atomicBackups) {
+                       try {
+                               String target = atomickBackup.backup(fileSystemManager, backupsBase, backupContext, opts);
+                               if (log.isDebugEnabled())
+                                       log.debug("Performed backup " + target);
+                       } catch (Exception e) {
+                               String msg = "Atomic backup " + atomickBackup.getName() + " failed: "
+                                               + LangUtils.chainCausesMessages(e);
+                               failures.add(msg);
+                               log.error(msg);
+                               if (log.isTraceEnabled())
+                                       log.trace("Stacktrace of atomic backup " + atomickBackup.getName() + " failure.", e);
+                       }
+               }
+
+               // dispatch to remote
+               for (String remoteBase : remoteBases.keySet()) {
+                       FileObject localBaseFo = null;
+                       FileObject remoteBaseFo = null;
+                       UserAuthenticator auth = remoteBases.get(remoteBase);
+
+                       // authentication
+                       FileSystemOptions remoteOpts = new FileSystemOptions();
+                       try {
+                               DefaultFileSystemConfigBuilder.getInstance().setUserAuthenticator(remoteOpts, auth);
+                               backupPurge.purge(fileSystemManager, remoteBase, systemName, backupContext.getDateFormat(), remoteOpts);
+                       } catch (Exception e) {
+                               failures.add("Purge " + remoteBase + " failed: " + e.getMessage());
+                               log.error("Cannot purge " + remoteBase, e);
+                       }
+
+                       try {
+                               localBaseFo = fileSystemManager.resolveFile(backupsBase + '/' + backupContext.getRelativeFolder(),
+                                               opts);
+                               remoteBaseFo = fileSystemManager.resolveFile(remoteBase + '/' + backupContext.getRelativeFolder(),
+                                               remoteOpts);
+                               remoteBaseFo.copyFrom(localBaseFo, Selectors.SELECT_ALL);
+                               if (log.isDebugEnabled())
+                                       log.debug("Copied backup to " + remoteBaseFo + " from " + localBaseFo);
+                               // }
+                       } catch (Exception e) {
+                               failures.add("Dispatch to " + remoteBase + " failed: " + e.getMessage());
+                               log.error("Cannot dispatch backups from " + backupContext.getRelativeFolder() + " to " + remoteBase, e);
+                       }
+                       BackupUtils.closeFOQuietly(localBaseFo);
+                       BackupUtils.closeFOQuietly(remoteBaseFo);
+               }
+
+               int failureCount = 0;
+               if (failures.size() > 0) {
+                       StringBuffer buf = new StringBuffer();
+                       for (String failure : failures) {
+                               buf.append('\n').append(failureCount).append(" - ").append(failure);
+                               failureCount++;
+                       }
+                       throw new MaintenanceException(failureCount + " error(s) when running the backup,"
+                                       + " check the logs and the backups as soon as possible." + buf);
+               }
+       }
+
+       public void setFileSystemManager(FileSystemManager fileSystemManager) {
+               this.fileSystemManager = fileSystemManager;
+       }
+
+       public void setBackupsBase(String backupsBase) {
+               this.backupsBase = backupsBase;
+       }
+
+       public void setSystemName(String name) {
+               this.systemName = name;
+       }
+
+       public void setAtomicBackups(List<AtomicBackup> atomicBackups) {
+               this.atomicBackups = atomicBackups;
+       }
+
+       public void setBackupPurge(BackupPurge backupPurge) {
+               this.backupPurge = backupPurge;
+       }
+
+       public void setUserAuthenticator(UserAuthenticator userAuthenticator) {
+               this.userAuthenticator = userAuthenticator;
+       }
+
+       public void setRemoteBases(Map<String, UserAuthenticator> remoteBases) {
+               this.remoteBases = remoteBases;
+       }
+
+       // public static void main(String args[]) {
+       // while (true) {
+       // try {
+       // StandardFileSystemManager fsm = new StandardFileSystemManager();
+       // fsm.init();
+       //
+       // SystemBackup systemBackup = new SystemBackup();
+       // systemBackup.setSystemName("mySystem");
+       // systemBackup
+       // .setBackupsBase("/home/mbaudier/dev/src/commons/server/runtime/org.argeo.server.core/target");
+       // systemBackup.setFileSystemManager(fsm);
+       //
+       // List<AtomicBackup> atomicBackups = new ArrayList<AtomicBackup>();
+       //
+       // MySqlBackup mySqlBackup = new MySqlBackup("root", "", "test");
+       // atomicBackups.add(mySqlBackup);
+       // PostgreSqlBackup postgreSqlBackup = new PostgreSqlBackup(
+       // "argeo", "argeo", "gis_template");
+       // atomicBackups.add(postgreSqlBackup);
+       // SvnBackup svnBackup = new SvnBackup(
+       // "/home/mbaudier/tmp/testsvnrepo");
+       // atomicBackups.add(svnBackup);
+       //
+       // systemBackup.setAtomicBackups(atomicBackups);
+       //
+       // Map<String, UserAuthenticator> remoteBases = new HashMap<String,
+       // UserAuthenticator>();
+       // StaticUserAuthenticator userAuthenticator = new StaticUserAuthenticator(
+       // null, "demo", "demo");
+       // remoteBases.put("sftp://localhost/home/mbaudier/test",
+       // userAuthenticator);
+       // systemBackup.setRemoteBases(remoteBases);
+       //
+       // systemBackup.run();
+       //
+       // fsm.close();
+       // } catch (FileSystemException e) {
+       // // TODO Auto-generated catch block
+       // e.printStackTrace();
+       // System.exit(1);
+       // }
+       //
+       // // wait
+       // try {
+       // Thread.sleep(120 * 1000);
+       // } catch (InterruptedException e) {
+       // e.printStackTrace();
+       // }
+       // }
+       // }
+}
diff --git a/org.argeo.node.api/.classpath b/org.argeo.node.api/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.node.api/.gitignore b/org.argeo.node.api/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.node.api/.project b/org.argeo.node.api/.project
new file mode 100644 (file)
index 0000000..9573f0c
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.node.api</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.node.api/META-INF/.gitignore b/org.argeo.node.api/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.node.api/bnd.bnd b/org.argeo.node.api/bnd.bnd
new file mode 100644 (file)
index 0000000..52a2bdd
--- /dev/null
@@ -0,0 +1 @@
+Provide-Capability: cms.datamodel;name=node;cnd=/org/argeo/node/node.cnd
\ No newline at end of file
diff --git a/org.argeo.node.api/build.properties b/org.argeo.node.api/build.properties
new file mode 100644 (file)
index 0000000..34d2e4d
--- /dev/null
@@ -0,0 +1,4 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .
diff --git a/org.argeo.node.api/pom.xml b/org.argeo.node.api/pom.xml
new file mode 100644 (file)
index 0000000..76add3d
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.node.api</artifactId>
+       <name>Argeo Node API</name>
+       <packaging>jar</packaging>
+       <dependencies>
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/org.argeo.node.api/src/org/argeo/node/ArgeoLogListener.java b/org.argeo.node.api/src/org/argeo/node/ArgeoLogListener.java
new file mode 100644 (file)
index 0000000..303bbef
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node;
+
+/** Framework agnostic interface for log notifications */
+public interface ArgeoLogListener {
+       /**
+        * Appends a log
+        * 
+        * @param username
+        *            authentified user, null for anonymous
+        * @param level
+        *            INFO, DEBUG, WARN, etc. (logging framework specific)
+        * @param category
+        *            hierarchy (logging framework specific)
+        * @param thread
+        *            name of the thread which logged this message
+        * @param msg
+        *            any object as long as its toString() method returns the
+        *            message
+        * @param exception
+        *            exception in log4j ThrowableStrRep format
+        */
+       public void appendLog(String username, Long timestamp, String level,
+                       String category, String thread, Object msg, String[] exception);
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/ArgeoLogger.java b/org.argeo.node.api/src/org/argeo/node/ArgeoLogger.java
new file mode 100644 (file)
index 0000000..213286d
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node;
+
+/**
+ * Logging framework agnostic identifying a logging service, to which one can
+ * register
+ */
+public interface ArgeoLogger {
+       /**
+        * Register for events by threads with the same authentication (or all
+        * threads if admin)
+        */
+       public void register(ArgeoLogListener listener,
+                       Integer numberOfPreviousEvents);
+
+       /**
+        * For admin use only: register for all users
+        * 
+        * @param listener
+        *            the log listener
+        * @param numberOfPreviousEvents
+        *            the number of previous events to notify
+        * @param everything
+        *            if true even anonymous is logged
+        */
+       public void registerForAll(ArgeoLogListener listener,
+                       Integer numberOfPreviousEvents, boolean everything);
+
+       public void unregister(ArgeoLogListener listener);
+
+       public void unregisterForAll(ArgeoLogListener listener);
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/DataModelNamespace.java b/org.argeo.node.api/src/org/argeo/node/DataModelNamespace.java
new file mode 100644 (file)
index 0000000..58e4a64
--- /dev/null
@@ -0,0 +1,18 @@
+package org.argeo.node;
+
+import org.osgi.resource.Namespace;
+
+/** CMS Data Model capability namespace. */
+public class DataModelNamespace extends Namespace {
+
+       public static final String CMS_DATA_MODEL_NAMESPACE = "cms.datamodel";
+       public static final String NAME = "name";
+       public static final String CND = "cnd";
+       /** If 'true', indicates that no repository should be published */
+       public static final String ABSTRACT = "abstract";
+
+       private DataModelNamespace() {
+               // empty
+       }
+
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/NodeConstants.java b/org.argeo.node.api/src/org/argeo/node/NodeConstants.java
new file mode 100644 (file)
index 0000000..31029d9
--- /dev/null
@@ -0,0 +1,125 @@
+package org.argeo.node;
+
+public interface NodeConstants {
+       /*
+        * DN ATTRIBUTES (RFC 4514)
+        */
+       String CN = "cn";
+       String L = "l";
+       String ST = "st";
+       String O = "o";
+       String OU = "ou";
+       String C = "c";
+       String STREET = "street";
+       String DC = "dc";
+       String UID = "uid";
+
+       /*
+        * STANDARD ATTRIBUTES
+        */
+       String LABELED_URI = "labeledUri";
+
+       /*
+        * COMMON NAMES
+        */
+       String NODE = "node";
+       String HOME = "home";
+
+       /*
+        * BASE DNs
+        */
+       String DEPLOY_BASEDN = "ou=deploy,ou=node";
+
+       /*
+        * STANDARD VALUES
+        */
+       String DEFAULT = "default";
+
+       /*
+        * RESERVED ROLES
+        */
+       String ROLES_BASEDN = "ou=roles,ou=node";
+       String TOKENS_BASEDN = "ou=tokens,ou=node";
+       String ROLE_ADMIN = "cn=admin," + ROLES_BASEDN;
+       String ROLE_USER_ADMIN = "cn=userAdmin," + ROLES_BASEDN;
+       String ROLE_DATA_ADMIN = "cn=dataAdmin," + ROLES_BASEDN;
+       // Special system groups that cannot be edited:
+       // user U anonymous = everyone
+       String ROLE_USER = "cn=user," + ROLES_BASEDN;
+       String ROLE_ANONYMOUS = "cn=anonymous," + ROLES_BASEDN;
+       // Account lifecycle
+       String ROLE_REGISTERING = "cn=registering," + ROLES_BASEDN;
+
+       /*
+        * LOGIN CONTEXTS
+        */
+       String LOGIN_CONTEXT_NODE = "NODE";
+       String LOGIN_CONTEXT_USER = "USER";
+       String LOGIN_CONTEXT_ANONYMOUS = "ANONYMOUS";
+       String LOGIN_CONTEXT_DATA_ADMIN = "DATA_ADMIN";
+       String LOGIN_CONTEXT_SINGLE_USER = "SINGLE_USER";
+       String LOGIN_CONTEXT_KEYRING = "KEYRING";
+
+       /*
+        * PATHS
+        */
+       String PATH_DATA = "/data";
+       String PATH_JCR = "/jcr";
+       String PATH_FILES = "/files";
+       // String PATH_JCR_PUB = "/pub";
+
+       /*
+        * FILE SYSTEMS
+        */
+       String SCHEME_NODE = NODE;
+
+       /*
+        * KERBEROS
+        */
+       String NODE_SERVICE = NODE;
+
+       /*
+        * INIT FRAMEWORK PROPERTIES
+        */
+       String NODE_INIT = "argeo.node.init";
+       String I18N_DEFAULT_LOCALE = "argeo.i18n.defaultLocale";
+       String I18N_LOCALES = "argeo.i18n.locales";
+       // Node Security
+       String ROLES_URI = "argeo.node.roles.uri";
+       String TOKENS_URI = "argeo.node.tokens.uri";
+       /** URI to an LDIF file or LDAP server used as initialization or backend */
+       String USERADMIN_URIS = "argeo.node.useradmin.uris";
+       // Transaction manager
+       String TRANSACTION_MANAGER = "argeo.node.transaction.manager";
+       String TRANSACTION_MANAGER_SIMPLE = "simple";
+       String TRANSACTION_MANAGER_BITRONIX = "bitronix";
+       // Node
+       /** Properties configuring the node repository */
+       String NODE_REPO_PROP_PREFIX = "argeo.node.repo.";
+       /** Additional standalone repositories, related to data models. */
+       String NODE_REPOS_PROP_PREFIX = "argeo.node.repos.";
+       // HTTP
+       String HTTP_PORT = "org.osgi.service.http.port";
+       String HTTP_PORT_SECURE = "org.osgi.service.http.port.secure";
+       /**
+        * The HTTP header used to convey the DN of a client verified by a reverse
+        * proxy. Typically SSL_CLIENT_S_DN for Apache.
+        */
+       String HTTP_PROXY_SSL_DN = "argeo.http.proxy.ssl.dn";
+
+       /*
+        * PIDs
+        */
+       String NODE_STATE_PID = "org.argeo.node.state";
+       String NODE_DEPLOYMENT_PID = "org.argeo.node.deployment";
+       String NODE_INSTANCE_PID = "org.argeo.node.instance";
+
+       String NODE_KEYRING_PID = "org.argeo.node.keyring";
+       String NODE_FS_PROVIDER_PID = "org.argeo.node.fsProvider";
+
+       /*
+        * FACTORY PIDs
+        */
+       String NODE_REPOS_FACTORY_PID = "org.argeo.node.repos";
+       String NODE_USER_ADMIN_PID = "org.argeo.node.userAdmin";
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/NodeDeployment.java b/org.argeo.node.api/src/org/argeo/node/NodeDeployment.java
new file mode 100644 (file)
index 0000000..8e5558d
--- /dev/null
@@ -0,0 +1,5 @@
+package org.argeo.node;
+
+public interface NodeDeployment {
+       Long getAvailableSince();
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/NodeInstance.java b/org.argeo.node.api/src/org/argeo/node/NodeInstance.java
new file mode 100644 (file)
index 0000000..aa1b5ce
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.node;
+
+import javax.naming.ldap.LdapName;
+
+/** The structured data */
+public interface NodeInstance {
+       /**
+        * To be used as an identifier of a workgroup, typically as a value for the
+        * 'businessCategory' attribute in LDAP.
+        */
+       public final static String WORKGROUP = "workgroup";
+
+       /** Mark this group as a workgroup */
+       void createWorkgroup(LdapName groupDn);
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/NodeNames.java b/org.argeo.node.api/src/org/argeo/node/NodeNames.java
new file mode 100644 (file)
index 0000000..05b86ff
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node;
+
+/** JCR types in the http://www.argeo.org/node namespace */
+public interface NodeNames {
+       String LDAP_UID = "ldap:"+NodeConstants.UID;
+       String LDAP_CN = "ldap:"+NodeConstants.CN;
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/NodeOID.java b/org.argeo.node.api/src/org/argeo/node/NodeOID.java
new file mode 100644 (file)
index 0000000..387d511
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.node;
+
+interface NodeOID {
+       String BASE = "1.3.6.1.4.1" + ".48308" + ".1";
+
+       // ATTRIBUTE TYPES
+       String ATTRIBUTE_TYPES = BASE + ".4";
+
+       // OBJECT CLASSES
+       String OBJECT_CLASSES = BASE + ".6";
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/NodeState.java b/org.argeo.node.api/src/org/argeo/node/NodeState.java
new file mode 100644 (file)
index 0000000..d7148c6
--- /dev/null
@@ -0,0 +1,17 @@
+package org.argeo.node;
+
+import java.util.List;
+import java.util.Locale;
+
+public interface NodeState {
+       Locale getDefaultLocale();
+
+       List<Locale> getLocales();
+
+       String getHostname();
+
+       boolean isClean();
+       
+       Long getAvailableSince();
+
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/NodeTypes.java b/org.argeo.node.api/src/org/argeo/node/NodeTypes.java
new file mode 100644 (file)
index 0000000..bfb55e1
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node;
+
+/** JCR types in the http://www.argeo.org/node namespace */
+public interface NodeTypes {
+       String NODE_USER_HOME = "node:userHome";
+       String NODE_GROUP_HOME = "node:groupHome";
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/NodeUtils.java b/org.argeo.node.api/src/org/argeo/node/NodeUtils.java
new file mode 100644 (file)
index 0000000..afb64bd
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.RepositoryFactory;
+import javax.jcr.Session;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryResult;
+import javax.jcr.query.qom.Constraint;
+import javax.jcr.query.qom.DynamicOperand;
+import javax.jcr.query.qom.QueryObjectModelFactory;
+import javax.jcr.query.qom.Selector;
+import javax.jcr.query.qom.StaticOperand;
+
+/** Utilities related to Argeo model in JCR */
+public class NodeUtils {
+       /**
+        * Wraps the call to the repository factory based on parameter
+        * {@link NodeConstants#CN} in order to simplify it and
+        * protect against future API changes.
+        */
+       public static Repository getRepositoryByAlias(RepositoryFactory repositoryFactory, String alias) {
+               try {
+                       Map<String, String> parameters = new HashMap<String, String>();
+                       parameters.put(NodeConstants.CN, alias);
+                       return repositoryFactory.getRepository(parameters);
+               } catch (RepositoryException e) {
+                       throw new RuntimeException("Unexpected exception when trying to retrieve repository with alias " + alias,
+                                       e);
+               }
+       }
+
+       /**
+        * Wraps the call to the repository factory based on parameter
+        * {@link NodeConstants#LABELED_URI} in order to simplify it and
+        * protect against future API changes.
+        */
+       public static Repository getRepositoryByUri(RepositoryFactory repositoryFactory, String uri) {
+               return getRepositoryByUri(repositoryFactory, uri, null);
+       }
+
+       /**
+        * Wraps the call to the repository factory based on parameter
+        * {@link NodeConstants#LABELED_URI} in order to simplify it and
+        * protect against future API changes.
+        */
+       public static Repository getRepositoryByUri(RepositoryFactory repositoryFactory, String uri, String alias) {
+               try {
+                       Map<String, String> parameters = new HashMap<String, String>();
+                       parameters.put(NodeConstants.LABELED_URI, uri);
+                       if (alias != null)
+                               parameters.put(NodeConstants.CN, alias);
+                       return repositoryFactory.getRepository(parameters);
+               } catch (RepositoryException e) {
+                       throw new RuntimeException("Unexpected exception when trying to retrieve repository with uri " + uri, e);
+               }
+       }
+
+       private NodeUtils() {
+       }
+
+       /**
+        * Returns the home node of the user or null if none was found.
+        * 
+        * @param session
+        *            the session to use in order to perform the search, this can be
+        *            a session with a different user ID than the one searched,
+        *            typically when a system or admin session is used.
+        * @param username
+        *            the username of the user
+        */
+       public static Node getUserHome(Session session, String username) {
+               try {
+                       QueryObjectModelFactory qomf = session.getWorkspace().getQueryManager().getQOMFactory();
+                       Selector sel = qomf.selector(NodeTypes.NODE_USER_HOME, "sel");
+                       DynamicOperand dop = qomf.propertyValue(sel.getSelectorName(), NodeNames.LDAP_UID);
+                       StaticOperand sop = qomf.literal(session.getValueFactory().createValue(username));
+                       Constraint constraint = qomf.comparison(dop, QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, sop);
+                       Query query = qomf.createQuery(sel, constraint, null, null);
+                       return querySingleNode(query);
+               } catch (RepositoryException e) {
+                       throw new RuntimeException("Cannot find home for user " + username, e);
+               }
+       }
+
+       /**
+        * Returns the home node of the user or null if none was found.
+        * 
+        * @param session
+        *            the session to use in order to perform the search, this can be
+        *            a session with a different user ID than the one searched,
+        *            typically when a system or admin session is used.
+        * @param cn
+        *            the name of the group
+        */
+       public static Node getGroupHome(Session session, String cn) {
+               try {
+                       QueryObjectModelFactory qomf = session.getWorkspace().getQueryManager().getQOMFactory();
+                       Selector sel = qomf.selector(NodeTypes.NODE_GROUP_HOME, "sel");
+                       DynamicOperand dop = qomf.propertyValue(sel.getSelectorName(), NodeNames.LDAP_CN);
+                       StaticOperand sop = qomf.literal(session.getValueFactory().createValue(cn));
+                       Constraint constraint = qomf.comparison(dop, QueryObjectModelFactory.JCR_OPERATOR_EQUAL_TO, sop);
+                       Query query = qomf.createQuery(sel, constraint, null, null);
+                       return querySingleNode(query);
+               } catch (RepositoryException e) {
+                       throw new RuntimeException("Cannot find home for user " + cn, e);
+               }
+       }
+
+       /**
+        * Queries one single node.
+        * 
+        * @return one single node or null if none was found
+        * @throws ArgeoJcrException
+        *             if more than one node was found
+        */
+       private static Node querySingleNode(Query query) {
+               NodeIterator nodeIterator;
+               try {
+                       QueryResult queryResult = query.execute();
+                       nodeIterator = queryResult.getNodes();
+               } catch (RepositoryException e) {
+                       throw new RuntimeException("Cannot execute query " + query, e);
+               }
+               Node node;
+               if (nodeIterator.hasNext())
+                       node = nodeIterator.nextNode();
+               else
+                       return null;
+
+               if (nodeIterator.hasNext())
+                       throw new RuntimeException("Query returned more than one node.");
+               return node;
+       }
+
+       /** Returns the home node of the session user or null if none was found. */
+       public static Node getUserHome(Session session) {
+               String userID = session.getUserID();
+               return getUserHome(session, userID);
+       }
+
+       public static String getDataPath(String cn, Node node) throws RepositoryException {
+               assert node != null;
+               StringBuilder buf = new StringBuilder(NodeConstants.PATH_DATA);
+               return buf.append('/').append(cn).append('/').append(node.getSession().getWorkspace().getName())
+                               .append(node.getPath()).toString();
+       }
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/node.cnd b/org.argeo.node.api/src/org/argeo/node/node.cnd
new file mode 100644 (file)
index 0000000..f3d0619
--- /dev/null
@@ -0,0 +1,22 @@
+<ldap = 'http://www.argeo.org/ns/ldap'>
+<node = 'http://www.argeo.org/ns/node'>
+
+// DN (see https://tools.ietf.org/html/rfc4514)
+<cn = 'http://www.argeo.org/ns/rfc4514/cn'>
+<l = 'http://www.argeo.org/ns/rfc4514/l'>
+<st = 'http://www.argeo.org/ns/rfc4514/st'>
+<o = 'http://www.argeo.org/ns/rfc4514/o'>
+<ou = 'http://www.argeo.org/ns/rfc4514/ou'>
+<c = 'http://www.argeo.org/ns/rfc4514/c'>
+<street = 'http://www.argeo.org/ns/rfc4514/street'>
+<dc = 'http://www.argeo.org/ns/rfc4514/dc'>
+<uid = 'http://www.argeo.org/ns/rfc4514/uid'>
+
+
+[node:userHome]
+mixin
+- ldap:uid (STRING) m
+
+[node:groupHome]
+mixin
+- ldap:cn (STRING) m
diff --git a/org.argeo.node.api/src/org/argeo/node/package-info.java b/org.argeo.node.api/src/org/argeo/node/package-info.java
new file mode 100644 (file)
index 0000000..fda3bae
--- /dev/null
@@ -0,0 +1,5 @@
+/**
+ * Abstractions or constants related to an Argeo Node, an active repository of
+ * linked data.
+ */
+package org.argeo.node;
\ No newline at end of file
diff --git a/org.argeo.node.api/src/org/argeo/node/packageinfo b/org.argeo.node.api/src/org/argeo/node/packageinfo
new file mode 100644 (file)
index 0000000..2c9afe8
--- /dev/null
@@ -0,0 +1 @@
+version 2.1.0
\ No newline at end of file
diff --git a/org.argeo.node.api/src/org/argeo/node/security/AnonymousPrincipal.java b/org.argeo.node.api/src/org/argeo/node/security/AnonymousPrincipal.java
new file mode 100644 (file)
index 0000000..141f9d1
--- /dev/null
@@ -0,0 +1,36 @@
+package org.argeo.node.security;
+
+import java.security.Principal;
+
+import javax.naming.ldap.LdapName;
+
+import org.argeo.node.NodeConstants;
+
+/** Marker for anonymous users. */
+public final class AnonymousPrincipal implements Principal {
+       private final String name = NodeConstants.ROLE_ANONYMOUS;
+
+       @Override
+       public String getName() {
+               return name;
+       }
+
+       @Override
+       public int hashCode() {
+               return name.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               return this == obj;
+       }
+
+       @Override
+       public String toString() {
+               return name.toString();
+       }
+
+       public LdapName getLdapName(){
+               return NodeSecurityUtils.ROLE_ANONYMOUS_NAME;
+       }
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/security/CryptoKeyring.java b/org.argeo.node.api/src/org/argeo/node/security/CryptoKeyring.java
new file mode 100644 (file)
index 0000000..dd34022
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node.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.node.api/src/org/argeo/node/security/DataAdminPrincipal.java b/org.argeo.node.api/src/org/argeo/node/security/DataAdminPrincipal.java
new file mode 100644 (file)
index 0000000..53d2ced
--- /dev/null
@@ -0,0 +1,31 @@
+package org.argeo.node.security;
+
+import java.security.Principal;
+
+import org.argeo.node.NodeConstants;
+
+/** Allows to modify any data. */
+public final class DataAdminPrincipal implements Principal {
+       private final String name = NodeConstants.ROLE_DATA_ADMIN;
+
+       @Override
+       public String getName() {
+               return name;
+       }
+
+       @Override
+       public int hashCode() {
+               return name.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               return obj instanceof DataAdminPrincipal;
+       }
+
+       @Override
+       public String toString() {
+               return name.toString();
+       }
+
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/security/Keyring.java b/org.argeo.node.api/src/org/argeo/node/security/Keyring.java
new file mode 100644 (file)
index 0000000..fe054c3
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node.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.node.api/src/org/argeo/node/security/NodeSecurityUtils.java b/org.argeo.node.api/src/org/argeo/node/security/NodeSecurityUtils.java
new file mode 100644 (file)
index 0000000..7c784b0
--- /dev/null
@@ -0,0 +1,40 @@
+package org.argeo.node.security;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.node.NodeConstants;
+
+public class NodeSecurityUtils {
+       public final static LdapName ROLE_ADMIN_NAME, ROLE_DATA_ADMIN_NAME, ROLE_ANONYMOUS_NAME, ROLE_USER_NAME,
+                       ROLE_USER_ADMIN_NAME;
+       public final static List<LdapName> RESERVED_ROLES;
+       static {
+               try {
+                       ROLE_ADMIN_NAME = new LdapName(NodeConstants.ROLE_ADMIN);
+                       ROLE_DATA_ADMIN_NAME = new LdapName(NodeConstants.ROLE_DATA_ADMIN);
+                       ROLE_USER_NAME = new LdapName(NodeConstants.ROLE_USER);
+                       ROLE_USER_ADMIN_NAME = new LdapName(NodeConstants.ROLE_USER_ADMIN);
+                       ROLE_ANONYMOUS_NAME = new LdapName(NodeConstants.ROLE_ANONYMOUS);
+                       RESERVED_ROLES = Collections.unmodifiableList(Arrays.asList(
+                                       new LdapName[] { ROLE_ADMIN_NAME, ROLE_ANONYMOUS_NAME, ROLE_USER_NAME, ROLE_USER_ADMIN_NAME }));
+               } catch (InvalidNameException e) {
+                       throw new Error("Cannot initialize login module class", e);
+               }
+       }
+
+       public static void checkUserName(LdapName name) throws IllegalArgumentException {
+               if (RESERVED_ROLES.contains(name))
+                       throw new IllegalArgumentException(name + " is a reserved name");
+       }
+
+       public static void checkImpliedPrincipalName(LdapName roleName) throws IllegalArgumentException {
+//             if (ROLE_USER_NAME.equals(roleName) || ROLE_ANONYMOUS_NAME.equals(roleName))
+//                     throw new IllegalArgumentException(roleName + " cannot be listed as role");
+       }
+
+}
diff --git a/org.argeo.node.api/src/org/argeo/node/security/PBEKeySpecCallback.java b/org.argeo.node.api/src/org/argeo/node/security/PBEKeySpecCallback.java
new file mode 100644 (file)
index 0000000..f03ba9d
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node.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.node.api/src/org/argeo/node/tabular/ArrayTabularRow.java b/org.argeo.node.api/src/org/argeo/node/tabular/ArrayTabularRow.java
new file mode 100644 (file)
index 0000000..97bf025
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node.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.node.api/src/org/argeo/node/tabular/TabularColumn.java b/org.argeo.node.api/src/org/argeo/node/tabular/TabularColumn.java
new file mode 100644 (file)
index 0000000..5cd11d1
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node.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.node.api/src/org/argeo/node/tabular/TabularContent.java b/org.argeo.node.api/src/org/argeo/node/tabular/TabularContent.java
new file mode 100644 (file)
index 0000000..a5aa9f6
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node.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.node.api/src/org/argeo/node/tabular/TabularRow.java b/org.argeo.node.api/src/org/argeo/node/tabular/TabularRow.java
new file mode 100644 (file)
index 0000000..f652df9
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node.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.node.api/src/org/argeo/node/tabular/TabularRowIterator.java b/org.argeo.node.api/src/org/argeo/node/tabular/TabularRowIterator.java
new file mode 100644 (file)
index 0000000..98a04a6
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node.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.node.api/src/org/argeo/node/tabular/TabularWriter.java b/org.argeo.node.api/src/org/argeo/node/tabular/TabularWriter.java
new file mode 100644 (file)
index 0000000..b7febee
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.node.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.osgi.boot/.classpath b/org.argeo.osgi.boot/.classpath
new file mode 100644 (file)
index 0000000..a8a298a
--- /dev/null
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="src" path="ext/test"/>
+       <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-1.8"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.argeo.osgi.boot/.gitignore b/org.argeo.osgi.boot/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.osgi.boot/.project b/org.argeo.osgi.boot/.project
new file mode 100644 (file)
index 0000000..e145e96
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.osgi.boot</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>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+               <nature>org.eclipse.pde.PluginNature</nature>
+       </natures>
+</projectDescription>
diff --git a/org.argeo.osgi.boot/META-INF/.gitignore b/org.argeo.osgi.boot/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.osgi.boot/bnd.bnd b/org.argeo.osgi.boot/bnd.bnd
new file mode 100644 (file)
index 0000000..f0fa329
--- /dev/null
@@ -0,0 +1,5 @@
+Bundle-Activator: org.argeo.osgi.boot.Activator
+Import-Package: org.eclipse.*;resolution:=optional,\
+org.eclipse.osgi.launch.*;resolution:=optional,\
+org.osgi.*;version=0.0.0,\
+*
diff --git a/org.argeo.osgi.boot/build.properties b/org.argeo.osgi.boot/build.properties
new file mode 100644 (file)
index 0000000..406d799
--- /dev/null
@@ -0,0 +1,3 @@
+source.. = src/,\
+           ext/test/
+additional.bundles = org.junit
diff --git a/org.argeo.osgi.boot/ext/test/org/argeo/osgi/boot/OsgiBootNoRuntimeTest.java b/org.argeo.osgi.boot/ext/test/org/argeo/osgi/boot/OsgiBootNoRuntimeTest.java
new file mode 100644 (file)
index 0000000..c667f91
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.osgi.boot;
+
+import java.util.List;
+
+import junit.framework.TestCase;
+
+/** Tests which do not require a runtime. */
+@SuppressWarnings("rawtypes")
+public class OsgiBootNoRuntimeTest extends TestCase {
+       public final static String BUNDLES = "src/test/bundles/some;in=*;ex=excluded,"
+                       + "src/test/bundles/others;in=**/org.argeo.*";
+
+       /** Tests that location lists are properly parsed. */
+       // public void testLocations() {
+       // String baseUrl = "file:";
+       // String locations = "/mydir/myfile" + File.pathSeparator
+       // + "/myotherdir/myotherfile";
+       //
+       // OsgiBoot osgiBoot = new OsgiBoot(null);
+       // osgiBoot.setExcludeSvn(true);
+       // List urls = osgiBoot.getLocationsUrls(baseUrl, locations);
+       // assertEquals(2, urls.size());
+       // assertEquals("file:/mydir/myfile", urls.get(0));
+       // assertEquals("file:/myotherdir/myotherfile", urls.get(1));
+       // }
+
+       /** Tests that bundle lists are properly parsed. */
+       public void testBundles() {
+               String baseUrl = "file:";
+               String bundles = BUNDLES;
+               OsgiBoot osgiBoot = new OsgiBoot(null);
+//             osgiBoot.setExcludeSvn(true);
+               List urls = osgiBoot.getBundlesUrls(baseUrl, bundles);
+               for (int i = 0; i < urls.size(); i++)
+                       System.out.println(urls.get(i));
+               assertEquals(3, urls.size());
+
+               List jarUrls = osgiBoot.getBundlesUrls(baseUrl,
+                               "src/test/bundles/jars;in=*.jar");
+               for (int i = 0; i < jarUrls.size(); i++)
+                       System.out.println(jarUrls.get(i));
+               assertEquals(1, jarUrls.size());
+       }
+}
diff --git a/org.argeo.osgi.boot/ext/test/org/argeo/osgi/boot/OsgiBootRuntimeTest.java b/org.argeo.osgi.boot/ext/test/org/argeo/osgi/boot/OsgiBootRuntimeTest.java
new file mode 100644 (file)
index 0000000..b396c45
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.osgi.boot;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+import junit.framework.TestCase;
+
+import org.eclipse.core.runtime.adaptor.EclipseStarter;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+/** Starts an Equinox runtime and provision it with OSGi boot. */
+public class OsgiBootRuntimeTest extends TestCase {
+       protected OsgiBoot osgiBoot = null;
+       private boolean osgiRuntimeAlreadyRunning = false;
+
+       public void testInstallAndStart() throws Exception {
+               if (osgiRuntimeAlreadyRunning) {
+                       System.out
+                                       .println("OSGi runtime already running, skipping test...");
+                       return;
+               }
+               osgiBoot.installUrls(osgiBoot.getBundlesUrls(OsgiBoot.DEFAULT_BASE_URL,
+                               OsgiBootNoRuntimeTest.BUNDLES));
+               Map<String, Bundle> map = new TreeMap<String, Bundle>(
+                               osgiBoot.getBundlesBySymbolicName());
+               for (Iterator<String> keys = map.keySet().iterator(); keys.hasNext();) {
+                       String key = keys.next();
+                       Bundle bundle = map.get(key);
+                       System.out.println(key + " : " + bundle.getLocation());
+               }
+               assertEquals(4, map.size());
+               Iterator<String> keys = map.keySet().iterator();
+               assertEquals("org.argeo.osgi.boot.test.bundle1", keys.next());
+               assertEquals("org.argeo.osgi.boot.test.bundle2", keys.next());
+               assertEquals("org.argeo.osgi.boot.test.bundle3", keys.next());
+               assertEquals("org.eclipse.osgi", keys.next());
+
+               // osgiBoot.startBundles("org.argeo.osgi.boot.test.bundle2");
+               long begin = System.currentTimeMillis();
+               while (System.currentTimeMillis() - begin < 10000) {
+                       Map<String, Bundle> mapBundles = osgiBoot
+                                       .getBundlesBySymbolicName();
+                       Bundle bundle = mapBundles.get("org.argeo.osgi.boot.test.bundle2");
+                       if (bundle.getState() == Bundle.ACTIVE) {
+                               System.out.println("Bundle " + bundle + " started.");
+                               return;
+                       }
+               }
+               fail("Bundle not started after timeout limit.");
+       }
+
+       protected BundleContext startRuntime() throws Exception {
+               String[] args = { "-console", "-clean" };
+               BundleContext bundleContext = EclipseStarter.startup(args, null);
+
+//             ServiceLoader<FrameworkFactory> ff = ServiceLoader.load(FrameworkFactory.class);
+//             Map<String,String> config = new HashMap<String,String>();               
+//             Framework fwk = ff.iterator().next().newFramework(config);
+//             fwk.start();
+               return bundleContext;
+       }
+
+       protected void stopRuntime() throws Exception {
+               EclipseStarter.shutdown();
+       }
+
+       public void setUp() throws Exception {
+               osgiRuntimeAlreadyRunning = EclipseStarter.isRunning();
+               if (osgiRuntimeAlreadyRunning)
+                       return;
+               BundleContext bundleContext = startRuntime();
+               osgiBoot = new OsgiBoot(bundleContext);
+       }
+
+       public void tearDown() throws Exception {
+               if (osgiRuntimeAlreadyRunning)
+                       return;
+               osgiBoot = null;
+               stopRuntime();
+       }
+
+}
diff --git a/org.argeo.osgi.boot/pom.xml b/org.argeo.osgi.boot/pom.xml
new file mode 100644 (file)
index 0000000..9e663b7
--- /dev/null
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <version>2.1.76-SNAPSHOT</version>
+               <artifactId>argeo-commons</artifactId>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.osgi.boot</artifactId>
+       <packaging>jar</packaging>
+       <name>Commons OSGi Boot</name>
+       <build>
+               <plugins>
+                       <plugin>
+                               <artifactId>maven-surefire-plugin</artifactId>
+                               <configuration>
+                                       <skipTests>true</skipTests>
+                               </configuration>
+                       </plugin>
+               </plugins>
+       </build>
+       <dependencies>
+<!--           <dependency> -->
+<!--                   <groupId>org.argeo.tp</groupId> -->
+<!--                   <artifactId>argeo-tp</artifactId> -->
+<!--                   <version>${version.argeo-tp}</version> -->
+<!--                   <scope>provided</scope> -->
+<!--           </dependency> -->
+
+<!--           <dependency> -->
+<!--                   <groupId>org.argeo.tp.rap.platform</groupId> -->
+<!--                   <artifactId>org.eclipse.osgi</artifactId> -->
+<!--                   <scope>provided</scope> -->
+<!--           </dependency> -->
+
+               <!-- TEST -->
+<!--           <dependency> -->
+<!--                   <groupId>org.argeo.tp</groupId> -->
+<!--                   <artifactId>junit</artifactId> -->
+<!--                   <scope>test</scope> -->
+<!--           </dependency> -->
+       </dependencies>
+
+
+</project>
\ No newline at end of file
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/Activator.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/Activator.java
new file mode 100644 (file)
index 0000000..9e22b7b
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.osgi.boot;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+
+/**
+ * An OSGi configurator. See
+ * <a href="http://wiki.eclipse.org/Configurator">http:
+ * //wiki.eclipse.org/Configurator</a>
+ */
+public class Activator implements BundleActivator {
+       private Long checkpoint = null;
+
+       public void start(final BundleContext bundleContext) throws Exception {
+               // admin thread
+               Thread adminThread = new AdminThread(bundleContext);
+               adminThread.start();
+
+               // bootstrap
+               OsgiBoot osgiBoot = new OsgiBoot(bundleContext);
+               if (checkpoint == null) {
+                       osgiBoot.bootstrap();
+                       checkpoint = System.currentTimeMillis();
+               } else {
+                       osgiBoot.update();
+                       checkpoint = System.currentTimeMillis();
+               }
+       }
+
+       public void stop(BundleContext context) throws Exception {
+       }
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/AdminThread.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/AdminThread.java
new file mode 100644 (file)
index 0000000..6f01b89
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.osgi.boot;
+
+import java.io.File;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.launch.Framework;
+
+/** Monitors the runtime and can shut it down. */
+public class AdminThread extends Thread {
+       public final static String PROP_ARGEO_OSGI_SHUTDOWN_FILE = "argeo.osgi.shutdownFile";
+       private File shutdownFile;
+       private final BundleContext bundleContext;
+
+       public AdminThread(BundleContext bundleContext) {
+               super("OSGi Boot Admin");
+               this.bundleContext = bundleContext;
+               if (System.getProperty(PROP_ARGEO_OSGI_SHUTDOWN_FILE) != null) {
+                       shutdownFile = new File(
+                                       System.getProperty(PROP_ARGEO_OSGI_SHUTDOWN_FILE));
+                       if (!shutdownFile.exists()) {
+                               shutdownFile = null;
+                               OsgiBootUtils.warn("Shutdown file " + shutdownFile
+                                               + " not found, feature deactivated");
+                       }
+               }
+       }
+
+       public void run() {
+               if (shutdownFile != null) {
+                       // wait for file to be removed
+                       while (shutdownFile.exists()) {
+                               try {
+                                       Thread.sleep(1000);
+                               } catch (InterruptedException e) {
+                                       e.printStackTrace();
+                               }
+                       }
+
+                       Framework framework = (Framework) bundleContext.getBundle(0);
+                       try {
+                               // shutdown framework
+                               framework.stop();
+                               // wait 10 mins for shutdown
+                               framework.waitForStop(10 * 60 * 1000);
+                               // close VM
+                               System.exit(0);
+                       } catch (Exception e) {
+                               e.printStackTrace();
+                               System.exit(1);
+                       }
+               }
+       }
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/BundlesSet.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/BundlesSet.java
new file mode 100644 (file)
index 0000000..4a342fd
--- /dev/null
@@ -0,0 +1,71 @@
+package org.argeo.osgi.boot;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/** Intermediary structure used by path matching */
+class BundlesSet {
+       private String baseUrl = "reference:file";// not used yet
+       private final String dir;
+       private List<String> includes = new ArrayList<String>();
+       private List<String> excludes = new ArrayList<String>();
+
+       public BundlesSet(String def) {
+               StringTokenizer st = new StringTokenizer(def, ";");
+
+               if (!st.hasMoreTokens())
+                       throw new RuntimeException("Base dir not defined.");
+               try {
+                       String dirPath = st.nextToken();
+
+                       if (dirPath.startsWith("file:"))
+                               dirPath = dirPath.substring("file:".length());
+
+                       dir = new File(dirPath.replace('/', File.separatorChar)).getCanonicalPath();
+                       if (OsgiBootUtils.debug)
+                               OsgiBootUtils.debug("Base dir: " + dir);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot convert to absolute path", e);
+               }
+
+               while (st.hasMoreTokens()) {
+                       String tk = st.nextToken();
+                       StringTokenizer stEq = new StringTokenizer(tk, "=");
+                       String type = stEq.nextToken();
+                       String pattern = stEq.nextToken();
+                       if ("in".equals(type) || "include".equals(type)) {
+                               includes.add(pattern);
+                       } else if ("ex".equals(type) || "exclude".equals(type)) {
+                               excludes.add(pattern);
+                       } else if ("baseUrl".equals(type)) {
+                               baseUrl = pattern;
+                       } else {
+                               System.err.println("Unkown bundles pattern type " + type);
+                       }
+               }
+
+               // if (excludeSvn && !excludes.contains(EXCLUDES_SVN_PATTERN)) {
+               // excludes.add(EXCLUDES_SVN_PATTERN);
+               // }
+       }
+
+       public String getDir() {
+               return dir;
+       }
+
+       public List<String> getIncludes() {
+               return includes;
+       }
+
+       public List<String> getExcludes() {
+               return excludes;
+       }
+
+       public String getBaseUrl() {
+               return baseUrl;
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/DistributionBundle.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/DistributionBundle.java
new file mode 100644 (file)
index 0000000..85bc6d5
--- /dev/null
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.osgi.boot;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+
+/**
+ * A distribution bundle is a bundle within a maven-like distribution
+ * groupId:Bundle-SymbolicName:Bundle-Version which references others OSGi
+ * bundle. It is not required to be OSGi complete also it will generally be
+ * expected that it is. The root of the repository is computed based on the file
+ * name of the URL and of the content of the index.
+ */
+public class DistributionBundle {
+       private final static String INDEX_FILE_NAME = "modularDistribution.csv";
+
+       private final String url;
+
+       private Manifest manifest;
+       private String symbolicName;
+       private String version;
+
+       /** can be null */
+       private String baseUrl;
+       /** can be null */
+       private String relativeUrl;
+       private String localCache;
+
+       private List<OsgiArtifact> artifacts;
+
+       private String separator = ",";
+
+       public DistributionBundle(String url) {
+               this.url = url;
+       }
+
+       public DistributionBundle(String baseUrl, String relativeUrl, String localCache) {
+               if (baseUrl == null || !baseUrl.endsWith("/"))
+                       throw new OsgiBootException("Base url " + baseUrl + " badly formatted");
+               if (relativeUrl.startsWith("http") || relativeUrl.startsWith("file:"))
+                       throw new OsgiBootException("Relative URL " + relativeUrl + " badly formatted");
+               this.url = constructUrl(baseUrl, relativeUrl);
+               this.baseUrl = baseUrl;
+               this.relativeUrl = relativeUrl;
+               this.localCache = localCache;
+       }
+
+       protected String constructUrl(String baseUrl, String relativeUrl) {
+               try {
+                       if (relativeUrl.indexOf('*') >= 0) {
+                               if (!baseUrl.startsWith("file:"))
+                                       throw new IllegalArgumentException(
+                                                       "Wildcard support only for file:, badly formatted " + baseUrl + " and " + relativeUrl);
+                               Path basePath = Paths.get(new URI(baseUrl));
+                               // Path basePath = Paths.get(new URI(baseUrl));
+                               // int li = relativeUrl.lastIndexOf('/');
+                               // String relativeDir = relativeUrl.substring(0, li);
+                               // String relativeFile = relativeUrl.substring(li,
+                               // relativeUrl.length());
+                               String pattern = "glob:" + basePath + '/' + relativeUrl;
+                               PathMatcher pm = basePath.getFileSystem().getPathMatcher(pattern);
+                               SortedMap<Version, Path> res = new TreeMap<>();
+                               checkDir(basePath, pm, res);
+                               if (res.size() == 0)
+                                       throw new OsgiBootException("No file matching " + relativeUrl + " found in " + baseUrl);
+                               return res.get(res.firstKey()).toUri().toString();
+                       } else {
+                               return baseUrl + relativeUrl;
+                       }
+               } catch (Exception e) {
+                       throw new OsgiBootException("Cannot build URL from " + baseUrl + " and " + relativeUrl, e);
+               }
+       }
+
+       private void checkDir(Path dir, PathMatcher pm, SortedMap<Version, Path> res) throws IOException {
+               try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
+                       for (Path path : ds) {
+                               if (Files.isDirectory(path))
+                                       checkDir(path, pm, res);
+                               else if (pm.matches(path)) {
+                                       String fileName = path.getFileName().toString();
+                                       fileName = fileName.substring(0, fileName.lastIndexOf('.'));
+                                       if (fileName.endsWith("-SNAPSHOT"))
+                                               fileName = fileName.substring(0, fileName.lastIndexOf('-')) + ".SNAPSHOT";
+                                       fileName = fileName.substring(fileName.lastIndexOf('-') + 1);
+                                       Version version = new Version(fileName);
+                                       res.put(version, path);
+                               }
+                       }
+               }
+       }
+
+       public void processUrl() {
+               JarInputStream jarIn = null;
+               try {
+                       URL u = new URL(url);
+
+                       // local cache
+                       URI localUri = new URI(localCache + relativeUrl);
+                       Path localPath = Paths.get(localUri);
+                       if (Files.exists(localPath))
+                               u = localUri.toURL();
+                       jarIn = new JarInputStream(u.openStream());
+
+                       // meta data
+                       manifest = jarIn.getManifest();
+                       symbolicName = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+                       version = manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+
+                       JarEntry indexEntry;
+                       while ((indexEntry = jarIn.getNextJarEntry()) != null) {
+                               String entryName = indexEntry.getName();
+                               if (entryName.equals(INDEX_FILE_NAME)) {
+                                       break;
+                               }
+                               jarIn.closeEntry();
+                       }
+
+                       // list artifacts
+                       if (indexEntry == null)
+                               throw new OsgiBootException("No index " + INDEX_FILE_NAME + " in " + url);
+                       artifacts = listArtifacts(jarIn);
+                       jarIn.closeEntry();
+
+                       // find base URL
+                       // won't work if distribution artifact is not listed
+                       for (int i = 0; i < artifacts.size(); i++) {
+                               OsgiArtifact osgiArtifact = (OsgiArtifact) artifacts.get(i);
+                               if (osgiArtifact.getSymbolicName().equals(symbolicName) && osgiArtifact.getVersion().equals(version)) {
+                                       String relativeUrl = osgiArtifact.getRelativeUrl();
+                                       if (url.endsWith(relativeUrl)) {
+                                               baseUrl = url.substring(0, url.length() - osgiArtifact.getRelativeUrl().length());
+                                               break;
+                                       }
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new OsgiBootException("Cannot list URLs from " + url, e);
+               } finally {
+                       if (jarIn != null)
+                               try {
+                                       jarIn.close();
+                               } catch (IOException e) {
+                                       // silent
+                               }
+               }
+       }
+
+       protected List<OsgiArtifact> listArtifacts(InputStream in) {
+               List<OsgiArtifact> osgiArtifacts = new ArrayList<OsgiArtifact>();
+               BufferedReader reader = null;
+               try {
+                       reader = new BufferedReader(new InputStreamReader(in));
+                       String line = null;
+                       lines: while ((line = reader.readLine()) != null) {
+                               StringTokenizer st = new StringTokenizer(line, separator);
+                               String moduleName = st.nextToken();
+                               String moduleVersion = st.nextToken();
+                               String relativeUrl = st.nextToken();
+                               if (relativeUrl.endsWith(".pom"))
+                                       continue lines;
+                               osgiArtifacts.add(new OsgiArtifact(moduleName, moduleVersion, relativeUrl));
+                       }
+               } catch (Exception e) {
+                       throw new OsgiBootException("Cannot list artifacts", e);
+               }
+               return osgiArtifacts;
+       }
+
+       /** Convenience method */
+       public static DistributionBundle processUrl(String baseUrl, String relativeUrl, String localCache) {
+               DistributionBundle distributionBundle = new DistributionBundle(baseUrl, relativeUrl, localCache);
+               distributionBundle.processUrl();
+               return distributionBundle;
+       }
+
+       /**
+        * List full URLs of the bundles, based on base URL, usable directly for
+        * download.
+        */
+       public List<String> listUrls() {
+               if (baseUrl == null)
+                       throw new OsgiBootException("Base URL is not set");
+
+               if (artifacts == null)
+                       throw new OsgiBootException("Artifact list not initialized");
+
+               List<String> urls = new ArrayList<String>();
+               for (int i = 0; i < artifacts.size(); i++) {
+                       OsgiArtifact osgiArtifact = (OsgiArtifact) artifacts.get(i);
+                       // local cache
+                       URI localUri;
+                       try {
+                               localUri = new URI(localCache + relativeUrl);
+                       } catch (URISyntaxException e) {
+                               OsgiBootUtils.warn(e.getMessage());
+                               localUri = null;
+                       }
+                       Version version = new Version(osgiArtifact.getVersion());
+                       if (localUri != null && Files.exists(Paths.get(localUri)) && version.getQualifier() != null
+                                       && version.getQualifier().startsWith("SNAPSHOT")) {
+                               urls.add(localCache + osgiArtifact.getRelativeUrl());
+                       } else {
+                               urls.add(baseUrl + osgiArtifact.getRelativeUrl());
+                       }
+               }
+               return urls;
+       }
+
+       public void setBaseUrl(String baseUrl) {
+               this.baseUrl = baseUrl;
+       }
+
+       /** Separator used to parse the tabular file */
+       public void setSeparator(String modulesUrlSeparator) {
+               this.separator = modulesUrlSeparator;
+       }
+
+       public String getRelativeUrl() {
+               return relativeUrl;
+       }
+
+       /** One of the listed artifact */
+       protected static class OsgiArtifact {
+               private final String symbolicName;
+               private final String version;
+               private final String relativeUrl;
+
+               public OsgiArtifact(String symbolicName, String version, String relativeUrl) {
+                       super();
+                       this.symbolicName = symbolicName;
+                       this.version = version;
+                       this.relativeUrl = relativeUrl;
+               }
+
+               public String getSymbolicName() {
+                       return symbolicName;
+               }
+
+               public String getVersion() {
+                       return version;
+               }
+
+               public String getRelativeUrl() {
+                       return relativeUrl;
+               }
+
+       }
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/Launcher.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/Launcher.java
new file mode 100644 (file)
index 0000000..7fd3e76
--- /dev/null
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.osgi.boot;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.core.runtime.adaptor.EclipseStarter;
+import org.osgi.framework.BundleContext;
+
+/** Command line interface. */
+public class Launcher {
+
+       public static void main(String[] args) {
+               // Try to load system properties
+               String systemPropertiesFilePath = getProperty(OsgiBoot.PROP_ARGEO_OSGI_BOOT_SYSTEM_PROPERTIES_FILE);
+               if (systemPropertiesFilePath != null) {
+                       FileInputStream in;
+                       try {
+                               in = new FileInputStream(systemPropertiesFilePath);
+                               System.getProperties().load(in);
+                       } catch (IOException e1) {
+                               throw new RuntimeException("Cannot load system properties from " + systemPropertiesFilePath, e1);
+                       }
+                       if (in != null) {
+                               try {
+                                       in.close();
+                               } catch (Exception e) {
+                                       // silent
+                               }
+                       }
+               }
+
+               // Start main class
+               startMainClass();
+
+               // Start Equinox
+               BundleContext bundleContext = null;
+               try {
+                       bundleContext = EclipseStarter.startup(args, null);
+               } catch (Exception e) {
+                       throw new RuntimeException("Cannot start Equinox.", e);
+               }
+
+               // OSGi bootstrap
+               OsgiBoot osgiBoot = new OsgiBoot(bundleContext);
+               osgiBoot.bootstrap();
+       }
+
+       protected static void startMainClass() {
+               String className = getProperty(OsgiBoot.PROP_ARGEO_OSGI_BOOT_APPCLASS);
+               if (className == null)
+                       return;
+
+               String line = System.getProperty(OsgiBoot.PROP_ARGEO_OSGI_BOOT_APPARGS, "");
+
+               String[] uiArgs = readArgumentsFromLine(line);
+
+               try {
+                       // Launch main method using reflection
+                       Class<?> clss = Class.forName(className);
+                       Class<?>[] mainArgsClasses = new Class[] { uiArgs.getClass() };
+                       Object[] mainArgs = { uiArgs };
+                       Method mainMethod = clss.getMethod("main", mainArgsClasses);
+                       mainMethod.invoke(null, mainArgs);
+               } catch (Exception e) {
+                       throw new RuntimeException("Cannot start main class.", e);
+               }
+
+       }
+
+       /**
+        * Transform a line into an array of arguments, taking "" as single
+        * arguments. (nested \" are not supported)
+        */
+       private static String[] readArgumentsFromLine(String lineOrig) {
+               String line = lineOrig.trim();// remove trailing spaces
+               List<String> args = new ArrayList<String>();
+               StringBuffer curr = new StringBuffer("");
+               boolean inQuote = false;
+               char[] arr = line.toCharArray();
+               for (int i = 0; i < arr.length; i++) {
+                       char c = arr[i];
+                       switch (c) {
+                       case '\"':
+                               inQuote = !inQuote;
+                               break;
+                       case ' ':
+                               if (!inQuote) {// otherwise, no break: goes to default
+                                       if (curr.length() > 0) {
+                                               args.add(curr.toString());
+                                               curr = new StringBuffer("");
+                                       }
+                                       break;
+                               }
+                       default:
+                               curr.append(c);
+                               break;
+                       }
+               }
+
+               // Add last arg
+               if (curr.length() > 0) {
+                       args.add(curr.toString());
+                       curr = null;
+               }
+
+               String[] res = new String[args.size()];
+               for (int i = 0; i < args.size(); i++) {
+                       res[i] = args.get(i).toString();
+               }
+               return res;
+       }
+
+       public static String getProperty(String name, String defaultValue) {
+               final String value;
+               if (defaultValue != null)
+                       value = System.getProperty(name, defaultValue);
+               else
+                       value = System.getProperty(name);
+
+               if (value == null || value.equals(""))
+                       return null;
+               else
+                       return value;
+       }
+
+       public static String getProperty(String name) {
+               return getProperty(name, null);
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/Main.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/Main.java
new file mode 100644 (file)
index 0000000..fdabd99
--- /dev/null
@@ -0,0 +1,32 @@
+package org.argeo.osgi.boot;
+
+import java.lang.management.ManagementFactory;
+
+public class Main {
+
+       public static void main(String[] args) {
+               String mainClass = System.getProperty(OsgiBoot.PROP_ARGEO_OSGI_BOOT_APPCLASS);
+               if (mainClass == null) {
+                       throw new IllegalArgumentException(
+                                       "System property " + OsgiBoot.PROP_ARGEO_OSGI_BOOT_APPCLASS + " must be specified");
+               }
+
+               OsgiBuilder osgi = new OsgiBuilder();
+               String distributionUrl = System.getProperty(OsgiBoot.PROP_ARGEO_OSGI_DISTRIBUTION_URL);
+               if (distributionUrl != null)
+                       osgi.install(distributionUrl);
+               // osgi.conf("argeo.node.useradmin.uris", "os:///");
+               // osgi.conf("osgi.clean", "true");
+               // osgi.conf("osgi.console", "true");
+               osgi.launch();
+               osgi.main(mainClass, args);
+
+               long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
+               String jvmUptimeStr = (jvmUptime / 1000) + "." + (jvmUptime % 1000) + "s";
+               System.out.println("Command " + mainClass + " executed in " + jvmUptimeStr);
+
+               osgi.shutdown();
+
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/NodeRunner.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/NodeRunner.java
new file mode 100644 (file)
index 0000000..263a4cd
--- /dev/null
@@ -0,0 +1,235 @@
+package org.argeo.osgi.boot;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ServiceLoader;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+
+/** Launch an OSGi framework and deploy a CMS Node into it. */
+public class NodeRunner {
+       private Long timeout = 30 * 1000l;
+       private final Path baseDir;
+       private final Path confDir;
+       private final Path dataDir;
+
+       private String baseUrl = "http://forge.argeo.org/data/java/argeo-2.1/";
+       private String distributionUrl = null;
+
+       private Framework framework = null;
+
+       public NodeRunner(String distributionUrl, Path baseDir) {
+               this.distributionUrl = distributionUrl;
+               Path mavenBase = Paths.get(System.getProperty("user.home") + "/.m2/repository");
+               Path osgiBase = Paths.get("/user/share/osgi");
+               if (Files.exists(mavenBase)) {
+                       Path mavenPath = mavenBase.resolve(distributionUrl);
+                       if (Files.exists(mavenPath))
+                               baseUrl = mavenBase.toUri().toString();
+               } else if (Files.exists(osgiBase)) {
+                       Path osgiPath = osgiBase.resolve(distributionUrl);
+                       if (Files.exists(osgiPath))
+                               baseUrl = osgiBase.toUri().toString();
+               }
+
+               this.baseDir = baseDir;
+               this.confDir = this.baseDir.resolve("state");
+               this.dataDir = this.baseDir.resolve("data");
+
+       }
+
+       public void start() {
+               long begin = System.currentTimeMillis();
+               // log4j
+               Path log4jFile = confDir.resolve("log4j.properties");
+               if (!Files.exists(log4jFile))
+                       copyResource("/org/argeo/osgi/boot/log4j.properties", log4jFile);
+               System.setProperty("log4j.configuration", "file://" + log4jFile.toAbsolutePath());
+
+               // Start Equinox
+               try {
+                       ServiceLoader<FrameworkFactory> ff = ServiceLoader.load(FrameworkFactory.class);
+                       FrameworkFactory frameworkFactory = ff.iterator().next();
+                       Map<String, String> configuration = new HashMap<String, String>();
+                       configuration.put("osgi.configuration.area", confDir.toAbsolutePath().toString());
+                       configuration.put("osgi.instance.area", dataDir.toAbsolutePath().toString());
+                       defaultConfiguration(configuration);
+
+                       framework = frameworkFactory.newFramework(configuration);
+                       framework.start();
+                       info("## Date : " + new Date());
+                       info("## Data : " + dataDir.toAbsolutePath());
+               } catch (Exception e) {
+                       throw new IllegalStateException("Cannot start OSGi framework", e);
+               }
+               BundleContext bundleContext = framework.getBundleContext();
+               try {
+
+                       // Spring configs currently require System properties
+                       // System.getProperties().putAll(configuration);
+
+                       // expected by JAAS as System.property FIXME
+                       System.setProperty("osgi.instance.area", bundleContext.getProperty("osgi.instance.area"));
+
+                       // OSGi bootstrap
+                       OsgiBoot osgiBoot = new OsgiBoot(bundleContext);
+
+                       osgiBoot.installUrls(osgiBoot.getDistributionUrls(distributionUrl, baseUrl));
+
+                       // Start runtime
+                       Properties startProperties = new Properties();
+                       // TODO make it possible to override it
+                       startProperties.put("argeo.osgi.start.2.node",
+                                       "org.eclipse.equinox.http.servlet,org.eclipse.equinox.http.jetty,"
+                                                       + "org.eclipse.equinox.metatype,org.eclipse.equinox.cm,org.eclipse.rap.rwt.osgi");
+                       startProperties.put("argeo.osgi.start.3.node", "org.argeo.cms");
+                       startProperties.put("argeo.osgi.start.4.node",
+                                       "org.eclipse.gemini.blueprint.extender,org.eclipse.equinox.http.registry");
+                       osgiBoot.startBundles(startProperties);
+
+                       // Find node repository
+                       ServiceReference<?> sr = null;
+                       while (sr == null) {
+                               sr = bundleContext.getServiceReference("javax.jcr.Repository");
+                               if (System.currentTimeMillis() - begin > timeout)
+                                       throw new RuntimeException("Could find node after " + timeout + "ms");
+                               Thread.sleep(100);
+                       }
+                       Object nodeDeployment = bundleContext.getService(sr);
+                       info("Node Deployment " + nodeDeployment);
+
+                       // Initialization completed
+                       long duration = System.currentTimeMillis() - begin;
+                       info("## CMS Launcher initialized in " + (duration / 1000) + "s " + (duration % 1000) + "ms");
+               } catch (Exception e) {
+                       shutdown();
+                       throw new RuntimeException("Cannot start CMS", e);
+               } finally {
+
+               }
+       }
+
+       private void defaultConfiguration(Map<String, String> configuration) {
+               // all permissions to OSGi security manager
+               Path policyFile = confDir.resolve("node.policy");
+               if (!Files.exists(policyFile))
+                       copyResource("/org/argeo/osgi/boot/node.policy", policyFile);
+               configuration.put("java.security.policy", "file://" + policyFile.toAbsolutePath());
+
+               configuration.put("org.eclipse.rap.workbenchAutostart", "false");
+               configuration.put("org.eclipse.equinox.http.jetty.autostart", "false");
+               configuration.put("org.osgi.framework.bootdelegation",
+                               "com.sun.jndi.ldap,com.sun.jndi.ldap.sasl,com.sun.security.jgss,com.sun.jndi.dns,"
+                                               + "com.sun.nio.file,com.sun.nio.sctp");
+
+               // Do clean
+               // configuration.put("osgi.clean", "true");
+               // if (args.length == 0) {
+               // configuration.put("osgi.console", "");
+               // }
+       }
+
+       public void shutdown() {
+               try {
+                       framework.stop();
+                       framework.waitForStop(15 * 1000);
+               } catch (Exception silent) {
+               }
+       }
+
+       public Path getConfDir() {
+               return confDir;
+       }
+
+       public Path getDataDir() {
+               return dataDir;
+       }
+
+       public Framework getFramework() {
+               return framework;
+       }
+
+       public static void main(String[] args) {
+               try {
+                       String distributionUrl;
+                       Path executionDir;
+                       if (args.length == 2) {
+                               distributionUrl = args[0];
+                               executionDir = Paths.get(args[1]);
+                       } else if (args.length == 1) {
+                               executionDir = Paths.get(System.getProperty("user.dir"));
+                               distributionUrl = args[0];
+                       } else if (args.length == 0) {
+                               executionDir = Paths.get(System.getProperty("user.dir"));
+                               distributionUrl = "org/argeo/commons/org.argeo.dep.cms.sdk/2.1.70/org.argeo.dep.cms.sdk-2.1.70.jar";
+                       }else{
+                               printUsage();
+                               System.exit(1);
+                               return;
+                       }
+
+                       NodeRunner nodeRunner = new NodeRunner(distributionUrl, executionDir);
+                       nodeRunner.start();
+//                     if (args.length != 0)
+//                             System.exit(0);
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       System.exit(1);
+               }
+       }
+
+       protected static void info(Object msg) {
+               System.out.println(msg);
+       }
+
+       protected static void err(Object msg) {
+               System.err.println(msg);
+       }
+
+       protected static void debug(Object msg) {
+               System.out.println(msg);
+       }
+
+       protected static void copyResource(String resource, Path targetFile) {
+               InputStream input = null;
+               OutputStream output = null;
+               try {
+                       input = NodeRunner.class.getResourceAsStream(resource);
+                       Files.createDirectories(targetFile.getParent());
+                       output = Files.newOutputStream(targetFile);
+                       byte[] buf = new byte[8192];
+                       while (true) {
+                               int length = input.read(buf);
+                               if (length < 0)
+                                       break;
+                               output.write(buf, 0, length);
+                       }
+               } catch (Exception e) {
+                       throw new RuntimeException("Cannot write " + resource + " file to " + targetFile, e);
+               } finally {
+                       try {
+                               input.close();
+                       } catch (Exception ignore) {
+                       }
+                       try {
+                               output.close();
+                       } catch (Exception ignore) {
+                       }
+               }
+
+       }
+
+       static void printUsage(){
+               err("Usage: <distribution url> <base dir>");
+       }
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBoot.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBoot.java
new file mode 100644 (file)
index 0000000..4c3ee40
--- /dev/null
@@ -0,0 +1,756 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.osgi.boot;
+
+import static org.argeo.osgi.boot.OsgiBootUtils.debug;
+import static org.argeo.osgi.boot.OsgiBootUtils.warn;
+
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+
+import org.argeo.osgi.boot.a2.ProvisioningManager;
+import org.argeo.osgi.boot.internal.springutil.AntPathMatcher;
+import org.argeo.osgi.boot.internal.springutil.PathMatcher;
+import org.argeo.osgi.boot.internal.springutil.SystemPropertyUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.Version;
+import org.osgi.framework.startlevel.BundleStartLevel;
+import org.osgi.framework.startlevel.FrameworkStartLevel;
+import org.osgi.framework.wiring.FrameworkWiring;
+
+/**
+ * Basic provisioning of an OSGi runtime via file path patterns and system
+ * properties. The approach is to generate list of URLs based on various
+ * methods, configured via properties.
+ */
+public class OsgiBoot implements OsgiBootConstants {
+       public final static String PROP_ARGEO_OSGI_START = "argeo.osgi.start";
+       public final static String PROP_ARGEO_OSGI_SOURCES = "argeo.osgi.sources";
+
+       public final static String PROP_ARGEO_OSGI_BUNDLES = "argeo.osgi.bundles";
+       public final static String PROP_ARGEO_OSGI_BASE_URL = "argeo.osgi.baseUrl";
+       public final static String PROP_ARGEO_OSGI_LOCAL_CACHE = "argeo.osgi.localCache";
+       public final static String PROP_ARGEO_OSGI_DISTRIBUTION_URL = "argeo.osgi.distributionUrl";
+
+       // booleans
+       public final static String PROP_ARGEO_OSGI_BOOT_DEBUG = "argeo.osgi.boot.debug";
+       // public final static String PROP_ARGEO_OSGI_BOOT_EXCLUDE_SVN =
+       // "argeo.osgi.boot.excludeSvn";
+
+       public final static String PROP_ARGEO_OSGI_BOOT_SYSTEM_PROPERTIES_FILE = "argeo.osgi.boot.systemPropertiesFile";
+       public final static String PROP_ARGEO_OSGI_BOOT_APPCLASS = "argeo.osgi.boot.appclass";
+       public final static String PROP_ARGEO_OSGI_BOOT_APPARGS = "argeo.osgi.boot.appargs";
+
+       public final static String DEFAULT_BASE_URL = "reference:file:";
+       // public final static String EXCLUDES_SVN_PATTERN = "**/.svn/**";
+
+       // OSGi system properties
+       final static String PROP_OSGI_BUNDLES_DEFAULTSTARTLEVEL = "osgi.bundles.defaultStartLevel";
+       final static String PROP_OSGI_STARTLEVEL = "osgi.startLevel";
+       final static String INSTANCE_AREA_PROP = "osgi.instance.area";
+       final static String CONFIGURATION_AREA_PROP = "osgi.configuration.area";
+
+       // Symbolic names
+       public final static String SYMBOLIC_NAME_OSGI_BOOT = "org.argeo.osgi.boot";
+       public final static String SYMBOLIC_NAME_EQUINOX = "org.eclipse.osgi";
+
+       /** Exclude svn metadata implicitely(a bit costly) */
+       // private boolean excludeSvn =
+       // Boolean.valueOf(System.getProperty(PROP_ARGEO_OSGI_BOOT_EXCLUDE_SVN,
+       // "false"))
+       // .booleanValue();
+
+       /** Default is 10s */
+       @Deprecated
+       private long defaultTimeout = 10000l;
+
+       private final BundleContext bundleContext;
+       private final String localCache;
+
+       private final ProvisioningManager provisioningManager;
+
+       /*
+        * INITIALIZATION
+        */
+       /** Constructor */
+       public OsgiBoot(BundleContext bundleContext) {
+               this.bundleContext = bundleContext;
+               String homeUri = Paths.get(System.getProperty("user.home")).toUri().toString();
+               localCache = getProperty(PROP_ARGEO_OSGI_LOCAL_CACHE, homeUri + ".m2/repository/");
+
+               provisioningManager = new ProvisioningManager(bundleContext);
+               String sources = getProperty(PROP_ARGEO_OSGI_SOURCES);
+               if (sources == null) {
+                       provisioningManager.registerDefaultSource();
+               } else {
+                       for (String source : sources.split(",")) {
+                               provisioningManager.registerSource(source);
+                       }
+               }
+       }
+
+       ProvisioningManager getProvisioningManager() {
+               return provisioningManager;
+       }
+
+       /*
+        * HIGH-LEVEL METHODS
+        */
+       /** Bootstraps the OSGi runtime */
+       public void bootstrap() {
+               try {
+                       long begin = System.currentTimeMillis();
+                       System.out.println();
+                       String osgiInstancePath = bundleContext.getProperty(INSTANCE_AREA_PROP);
+                       OsgiBootUtils
+                                       .info("OSGi bootstrap starting" + (osgiInstancePath != null ? " (" + osgiInstancePath + ")" : ""));
+                       installUrls(getBundlesUrls());
+                       installUrls(getDistributionUrls());
+                       provisioningManager.install(null);
+                       startBundles();
+                       long duration = System.currentTimeMillis() - begin;
+                       OsgiBootUtils.info("OSGi bootstrap completed in " + Math.round(((double) duration) / 1000) + "s ("
+                                       + duration + "ms), " + bundleContext.getBundles().length + " bundles");
+               } catch (RuntimeException e) {
+                       OsgiBootUtils.error("OSGi bootstrap FAILED", e);
+                       throw e;
+               }
+
+               // diagnostics
+               if (OsgiBootUtils.debug) {
+                       OsgiBootDiagnostics diagnostics = new OsgiBootDiagnostics(bundleContext);
+                       diagnostics.checkUnresolved();
+                       Map<String, Set<String>> duplicatePackages = diagnostics.findPackagesExportedTwice();
+                       if (duplicatePackages.size() > 0) {
+                               OsgiBootUtils.info("Packages exported twice:");
+                               Iterator<String> it = duplicatePackages.keySet().iterator();
+                               while (it.hasNext()) {
+                                       String pkgName = it.next();
+                                       OsgiBootUtils.info(pkgName);
+                                       Set<String> bdles = duplicatePackages.get(pkgName);
+                                       Iterator<String> bdlesIt = bdles.iterator();
+                                       while (bdlesIt.hasNext())
+                                               OsgiBootUtils.info("  " + bdlesIt.next());
+                               }
+                       }
+               }
+               System.out.println();
+       }
+
+       public void update() {
+               provisioningManager.update();
+       }
+
+       /*
+        * INSTALLATION
+        */
+       /** Install a single url. Convenience method. */
+       public Bundle installUrl(String url) {
+               List<String> urls = new ArrayList<String>();
+               urls.add(url);
+               installUrls(urls);
+               return (Bundle) getBundlesByLocation().get(url);
+       }
+
+       /** Install the bundles at this URL list. */
+       public void installUrls(List<String> urls) {
+               Map<String, Bundle> installedBundles = getBundlesByLocation();
+               for (int i = 0; i < urls.size(); i++) {
+                       String url = (String) urls.get(i);
+                       installUrl(url, installedBundles);
+               }
+               refreshFramework();
+       }
+
+       /** Actually install the provided URL */
+       protected void installUrl(String url, Map<String, Bundle> installedBundles) {
+               try {
+                       if (installedBundles.containsKey(url)) {
+                               Bundle bundle = (Bundle) installedBundles.get(url);
+                               if (OsgiBootUtils.debug)
+                                       debug("Bundle " + bundle.getSymbolicName() + " already installed from " + url);
+                       } else if (url.contains("/" + SYMBOLIC_NAME_EQUINOX + "/")
+                                       || url.contains("/" + SYMBOLIC_NAME_OSGI_BOOT + "/")) {
+                               if (OsgiBootUtils.debug)
+                                       warn("Skip " + url);
+                               return;
+                       } else {
+                               Bundle bundle = bundleContext.installBundle(url);
+                               if (url.startsWith("http"))
+                                       OsgiBootUtils
+                                                       .info("Installed " + bundle.getSymbolicName() + "-" + bundle.getVersion() + " from " + url);
+                               else if (OsgiBootUtils.debug)
+                                       OsgiBootUtils.debug(
+                                                       "Installed " + bundle.getSymbolicName() + "-" + bundle.getVersion() + " from " + url);
+                               assert bundle.getSymbolicName() != null;
+                               // uninstall previous versions
+                               bundles: for (Bundle b : bundleContext.getBundles()) {
+                                       if (b.getSymbolicName() == null)
+                                               continue bundles;
+                                       if (bundle.getSymbolicName().equals(b.getSymbolicName())) {
+                                               Version bundleV = bundle.getVersion();
+                                               Version bV = b.getVersion();
+                                               if (bV == null)
+                                                       continue bundles;
+                                               if (bundleV.getMajor() == bV.getMajor() && bundleV.getMinor() == bV.getMinor()) {
+                                                       if (bundleV.getMicro() > bV.getMicro()) {
+                                                               // uninstall older bundles
+                                                               b.uninstall();
+                                                               OsgiBootUtils.debug("Uninstalled " + b);
+                                                       } else if (bundleV.getMicro() < bV.getMicro()) {
+                                                               // uninstall just installed bundle if newer
+                                                               bundle.uninstall();
+                                                               OsgiBootUtils.debug("Uninstalled " + bundle);
+                                                               break bundles;
+                                                       } else {
+                                                               // uninstall any other with same major/minor
+                                                               if (!bundleV.getQualifier().equals(bV.getQualifier())) {
+                                                                       b.uninstall();
+                                                                       OsgiBootUtils.debug("Uninstalled " + b);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               } catch (BundleException e) {
+                       final String ALREADY_INSTALLED = "is already installed";
+                       String message = e.getMessage();
+                       if ((message.contains("Bundle \"" + SYMBOLIC_NAME_OSGI_BOOT + "\"")
+                                       || message.contains("Bundle \"" + SYMBOLIC_NAME_EQUINOX + "\""))
+                                       && message.contains(ALREADY_INSTALLED)) {
+                               // silent, in order to avoid warnings: we know that both
+                               // have already been installed...
+                       } else {
+                               if (message.contains(ALREADY_INSTALLED)) {
+                                       if (OsgiBootUtils.isDebug())
+                                               OsgiBootUtils.warn("Duplicate install from " + url + ": " + message);
+                               } else
+                                       OsgiBootUtils.warn("Could not install bundle from " + url + ": " + message);
+                       }
+                       if (OsgiBootUtils.debug && !message.contains(ALREADY_INSTALLED))
+                               e.printStackTrace();
+               }
+       }
+
+       /*
+        * START
+        */
+       public void startBundles() {
+               startBundles(System.getProperties());
+       }
+
+       public void startBundles(Properties properties) {
+               FrameworkStartLevel frameworkStartLevel = bundleContext.getBundle(0).adapt(FrameworkStartLevel.class);
+
+               // default and active start levels from System properties
+               Integer defaultStartLevel = new Integer(
+                               Integer.parseInt(getProperty(PROP_OSGI_BUNDLES_DEFAULTSTARTLEVEL, "4")));
+               Integer activeStartLevel = new Integer(getProperty(PROP_OSGI_STARTLEVEL, "6"));
+
+               SortedMap<Integer, List<String>> startLevels = new TreeMap<Integer, List<String>>();
+               computeStartLevels(startLevels, properties, defaultStartLevel);
+               // inverts the map for the time being, TODO optimise
+               Map<String, Integer> bundleStartLevels = new HashMap<>();
+               for (Integer level : startLevels.keySet()) {
+                       for (String bsn : startLevels.get(level))
+                               bundleStartLevels.put(bsn, level);
+               }
+               for (Bundle bundle : bundleContext.getBundles()) {
+                       String bsn = bundle.getSymbolicName();
+                       if (bundleStartLevels.containsKey(bsn)) {
+                               BundleStartLevel bundleStartLevel = bundle.adapt(BundleStartLevel.class);
+                               Integer level = bundleStartLevels.get(bsn);
+                               if (bundleStartLevel.getStartLevel() != level || !bundleStartLevel.isPersistentlyStarted()) {
+                                       bundleStartLevel.setStartLevel(level);
+                                       try {
+                                               bundle.start();
+                                       } catch (BundleException e) {
+                                               OsgiBootUtils.error("Cannot mark " + bsn + " as started", e);
+                                       }
+                                       if (getDebug())
+                                               OsgiBootUtils.debug(bsn + " starts at level " + level);
+                               }
+                       }
+               }
+               frameworkStartLevel.setStartLevel(activeStartLevel, (FrameworkEvent event) -> {
+                       if (getDebug())
+                               OsgiBootUtils.debug("Framework event: " + event);
+                       int initialStartLevel = frameworkStartLevel.getInitialBundleStartLevel();
+                       int startLevel = frameworkStartLevel.getStartLevel();
+                       OsgiBootUtils.debug("Framework start level: " + startLevel + " (initial: " + initialStartLevel + ")");
+               });
+       }
+
+       private static void computeStartLevels(SortedMap<Integer, List<String>> startLevels, Properties properties,
+                       Integer defaultStartLevel) {
+
+               // default (and previously, only behaviour)
+               appendToStartLevels(startLevels, defaultStartLevel, properties.getProperty(PROP_ARGEO_OSGI_START, ""));
+
+               // list argeo.osgi.start.* system properties
+               Iterator<Object> keys = properties.keySet().iterator();
+               final String prefix = PROP_ARGEO_OSGI_START + ".";
+               while (keys.hasNext()) {
+                       String key = keys.next().toString();
+                       if (key.startsWith(prefix)) {
+                               Integer startLevel;
+                               String suffix = key.substring(prefix.length());
+                               String[] tokens = suffix.split("\\.");
+                               if (tokens.length > 0 && !tokens[0].trim().equals(""))
+                                       try {
+                                               // first token is start level
+                                               startLevel = new Integer(tokens[0]);
+                                       } catch (NumberFormatException e) {
+                                               startLevel = defaultStartLevel;
+                                       }
+                               else
+                                       startLevel = defaultStartLevel;
+
+                               // append bundle names
+                               String bundleNames = properties.getProperty(key);
+                               appendToStartLevels(startLevels, startLevel, bundleNames);
+                       }
+               }
+       }
+
+       /** Append a comma-separated list of bundles to the start levels. */
+       private static void appendToStartLevels(SortedMap<Integer, List<String>> startLevels, Integer startLevel,
+                       String str) {
+               if (str == null || str.trim().equals(""))
+                       return;
+
+               if (!startLevels.containsKey(startLevel))
+                       startLevels.put(startLevel, new ArrayList<String>());
+               String[] bundleNames = str.split(",");
+               for (int i = 0; i < bundleNames.length; i++) {
+                       if (bundleNames[i] != null && !bundleNames[i].trim().equals(""))
+                               (startLevels.get(startLevel)).add(bundleNames[i]);
+               }
+       }
+
+       /**
+        * Start the provided list of bundles
+        *
+        * @return whether all bundles are now in active state
+        * @deprecated
+        */
+       @Deprecated
+       public boolean startBundles(List<String> bundlesToStart) {
+               if (bundlesToStart.size() == 0)
+                       return true;
+
+               // used to monitor ACTIVE states
+               List<Bundle> startedBundles = new ArrayList<Bundle>();
+               // used to log the bundles not found
+               List<String> notFoundBundles = new ArrayList<String>(bundlesToStart);
+
+               Bundle[] bundles = bundleContext.getBundles();
+               long startBegin = System.currentTimeMillis();
+               for (int i = 0; i < bundles.length; i++) {
+                       Bundle bundle = bundles[i];
+                       String symbolicName = bundle.getSymbolicName();
+                       if (bundlesToStart.contains(symbolicName))
+                               try {
+                                       try {
+                                               bundle.start();
+                                               if (OsgiBootUtils.debug)
+                                                       debug("Bundle " + symbolicName + " started");
+                                       } catch (Exception e) {
+                                               OsgiBootUtils.warn("Start of bundle " + symbolicName + " failed because of " + e
+                                                               + ", maybe bundle is not yet resolved," + " waiting and trying again.");
+                                               waitForBundleResolvedOrActive(startBegin, bundle);
+                                               bundle.start();
+                                               startedBundles.add(bundle);
+                                       }
+                                       notFoundBundles.remove(symbolicName);
+                               } catch (Exception e) {
+                                       OsgiBootUtils.warn("Bundle " + symbolicName + " cannot be started: " + e.getMessage());
+                                       if (OsgiBootUtils.debug)
+                                               e.printStackTrace();
+                                       // was found even if start failed
+                                       notFoundBundles.remove(symbolicName);
+                               }
+               }
+
+               for (int i = 0; i < notFoundBundles.size(); i++)
+                       OsgiBootUtils.warn("Bundle '" + notFoundBundles.get(i) + "' not started because it was not found.");
+
+               // monitors that all bundles are started
+               long beginMonitor = System.currentTimeMillis();
+               boolean allStarted = !(startedBundles.size() > 0);
+               List<String> notStarted = new ArrayList<String>();
+               while (!allStarted && (System.currentTimeMillis() - beginMonitor) < defaultTimeout) {
+                       notStarted = new ArrayList<String>();
+                       allStarted = true;
+                       for (int i = 0; i < startedBundles.size(); i++) {
+                               Bundle bundle = (Bundle) startedBundles.get(i);
+                               // TODO check behaviour of lazs bundles
+                               if (bundle.getState() != Bundle.ACTIVE) {
+                                       allStarted = false;
+                                       notStarted.add(bundle.getSymbolicName());
+                               }
+                       }
+                       try {
+                               Thread.sleep(100);
+                       } catch (InterruptedException e) {
+                               // silent
+                       }
+               }
+               long duration = System.currentTimeMillis() - beginMonitor;
+
+               if (!allStarted)
+                       for (int i = 0; i < notStarted.size(); i++)
+                               OsgiBootUtils.warn("Bundle '" + notStarted.get(i) + "' not ACTIVE after " + (duration / 1000) + "s");
+
+               return allStarted;
+       }
+
+       /** Waits for a bundle to become active or resolved */
+       @Deprecated
+       private void waitForBundleResolvedOrActive(long startBegin, Bundle bundle) throws Exception {
+               int originalState = bundle.getState();
+               if ((originalState == Bundle.RESOLVED) || (originalState == Bundle.ACTIVE))
+                       return;
+
+               String originalStateStr = OsgiBootUtils.stateAsString(originalState);
+
+               int currentState = bundle.getState();
+               while (!(currentState == Bundle.RESOLVED || currentState == Bundle.ACTIVE)) {
+                       long now = System.currentTimeMillis();
+                       if ((now - startBegin) > defaultTimeout * 10)
+                               throw new Exception("Bundle " + bundle.getSymbolicName() + " was not RESOLVED or ACTIVE after "
+                                               + (now - startBegin) + "ms (originalState=" + originalStateStr + ", currentState="
+                                               + OsgiBootUtils.stateAsString(currentState) + ")");
+
+                       try {
+                               Thread.sleep(100l);
+                       } catch (InterruptedException e) {
+                               // silent
+                       }
+                       currentState = bundle.getState();
+               }
+       }
+
+       /*
+        * BUNDLE PATTERNS INSTALLATION
+        */
+       /**
+        * Computes a list of URLs based on Ant-like include/exclude patterns defined by
+        * ${argeo.osgi.bundles} with the following format:<br>
+        * <code>/base/directory;in=*.jar;in=**;ex=org.eclipse.osgi_*;jar</code><br>
+        * WARNING: <code>/base/directory;in=*.jar,\</code> at the end of a file,
+        * without a new line causes a '.' to be appended with unexpected side effects.
+        */
+       public List<String> getBundlesUrls() {
+               String bundlePatterns = getProperty(PROP_ARGEO_OSGI_BUNDLES);
+               return getBundlesUrls(bundlePatterns);
+       }
+
+       /**
+        * Compute a list of URLs to install based on the provided patterns, with
+        * default base url
+        */
+       public List<String> getBundlesUrls(String bundlePatterns) {
+               String baseUrl = getProperty(PROP_ARGEO_OSGI_BASE_URL, DEFAULT_BASE_URL);
+               return getBundlesUrls(baseUrl, bundlePatterns);
+       }
+
+       /** Implements the path matching logic */
+       public List<String> getBundlesUrls(String baseUrl, String bundlePatterns) {
+               List<String> urls = new ArrayList<String>();
+               if (bundlePatterns == null)
+                       return urls;
+
+               bundlePatterns = SystemPropertyUtils.resolvePlaceholders(bundlePatterns);
+               if (OsgiBootUtils.debug)
+                       debug(PROP_ARGEO_OSGI_BUNDLES + "=" + bundlePatterns);
+
+               StringTokenizer st = new StringTokenizer(bundlePatterns, ",");
+               List<BundlesSet> bundlesSets = new ArrayList<BundlesSet>();
+               while (st.hasMoreTokens()) {
+                       String token = st.nextToken();
+                       if (new File(token).exists()) {
+                               String url = locationToUrl(baseUrl, token);
+                               urls.add(url);
+                       } else
+                               bundlesSets.add(new BundlesSet(token));
+               }
+
+               // find included
+               List<String> included = new ArrayList<String>();
+               PathMatcher matcher = new AntPathMatcher();
+               for (int i = 0; i < bundlesSets.size(); i++) {
+                       BundlesSet bundlesSet = (BundlesSet) bundlesSets.get(i);
+                       for (int j = 0; j < bundlesSet.getIncludes().size(); j++) {
+                               String pattern = (String) bundlesSet.getIncludes().get(j);
+                               match(matcher, included, bundlesSet.getDir(), null, pattern);
+                       }
+               }
+
+               // find excluded
+               List<String> excluded = new ArrayList<String>();
+               for (int i = 0; i < bundlesSets.size(); i++) {
+                       BundlesSet bundlesSet = (BundlesSet) bundlesSets.get(i);
+                       for (int j = 0; j < bundlesSet.getExcludes().size(); j++) {
+                               String pattern = (String) bundlesSet.getExcludes().get(j);
+                               match(matcher, excluded, bundlesSet.getDir(), null, pattern);
+                       }
+               }
+
+               // construct list
+               for (int i = 0; i < included.size(); i++) {
+                       String fullPath = (String) included.get(i);
+                       if (!excluded.contains(fullPath))
+                               urls.add(locationToUrl(baseUrl, fullPath));
+               }
+
+               return urls;
+       }
+
+       /*
+        * DISTRIBUTION JAR INSTALLATION
+        */
+       public List<String> getDistributionUrls() {
+               String distributionUrl = getProperty(PROP_ARGEO_OSGI_DISTRIBUTION_URL);
+               String baseUrl = getProperty(PROP_ARGEO_OSGI_BASE_URL);
+               return getDistributionUrls(distributionUrl, baseUrl);
+       }
+
+       public List<String> getDistributionUrls(String distributionUrl, String baseUrl) {
+               List<String> urls = new ArrayList<String>();
+               if (distributionUrl == null)
+                       return urls;
+
+               DistributionBundle distributionBundle;
+               if (distributionUrl.startsWith("http") || distributionUrl.startsWith("file")) {
+                       distributionBundle = new DistributionBundle(distributionUrl);
+                       if (baseUrl != null)
+                               distributionBundle.setBaseUrl(baseUrl);
+               } else {
+                       // relative url
+                       if (baseUrl == null) {
+                               baseUrl = localCache;
+                       }
+
+                       if (distributionUrl.contains(":")) {
+                               // TODO make it safer
+                               String[] parts = distributionUrl.trim().split(":");
+                               String[] categoryParts = parts[0].split("\\.");
+                               String artifactId = parts[1];
+                               String version = parts[2];
+                               StringBuilder sb = new StringBuilder();
+                               for (String categoryPart : categoryParts) {
+                                       sb.append(categoryPart).append('/');
+                               }
+                               sb.append(artifactId).append('/');
+                               sb.append(version).append('/');
+                               sb.append(artifactId).append('-').append(version).append(".jar");
+                               distributionUrl = sb.toString();
+                       }
+
+                       distributionBundle = new DistributionBundle(baseUrl, distributionUrl, localCache);
+               }
+               // if (baseUrl != null && !(distributionUrl.startsWith("http") ||
+               // distributionUrl.startsWith("file"))) {
+               // // relative url
+               // distributionBundle = new DistributionBundle(baseUrl, distributionUrl,
+               // localCache);
+               // } else {
+               // distributionBundle = new DistributionBundle(distributionUrl);
+               // if (baseUrl != null)
+               // distributionBundle.setBaseUrl(baseUrl);
+               // }
+               distributionBundle.processUrl();
+               return distributionBundle.listUrls();
+       }
+
+       /*
+        * HIGH LEVEL UTILITIES
+        */
+       /** Actually performs the matching logic. */
+       protected void match(PathMatcher matcher, List<String> matched, String base, String currentPath, String pattern) {
+               if (currentPath == null) {
+                       // Init
+                       File baseDir = new File(base.replace('/', File.separatorChar));
+                       File[] files = baseDir.listFiles();
+
+                       if (files == null) {
+                               if (OsgiBootUtils.debug)
+                                       OsgiBootUtils.warn("Base dir " + baseDir + " has no children, exists=" + baseDir.exists()
+                                                       + ", isDirectory=" + baseDir.isDirectory());
+                               return;
+                       }
+
+                       for (int i = 0; i < files.length; i++)
+                               match(matcher, matched, base, files[i].getName(), pattern);
+               } else {
+                       String fullPath = base + '/' + currentPath;
+                       if (matched.contains(fullPath))
+                               return;// don't try deeper if already matched
+
+                       boolean ok = matcher.match(pattern, currentPath);
+                       // if (debug)
+                       // debug(currentPath + " " + (ok ? "" : " not ")
+                       // + " matched with " + pattern);
+                       if (ok) {
+                               matched.add(fullPath);
+                               return;
+                       } else {
+                               String newFullPath = relativeToFullPath(base, currentPath);
+                               File newFile = new File(newFullPath);
+                               File[] files = newFile.listFiles();
+                               if (files != null) {
+                                       for (int i = 0; i < files.length; i++) {
+                                               String newCurrentPath = currentPath + '/' + files[i].getName();
+                                               if (files[i].isDirectory()) {
+                                                       if (matcher.matchStart(pattern, newCurrentPath)) {
+                                                               // recurse only if start matches
+                                                               match(matcher, matched, base, newCurrentPath, pattern);
+                                                       } else {
+                                                               if (OsgiBootUtils.debug)
+                                                                       debug(newCurrentPath + " does not start match with " + pattern);
+
+                                                       }
+                                               } else {
+                                                       boolean nonDirectoryOk = matcher.match(pattern, newCurrentPath);
+                                                       if (OsgiBootUtils.debug)
+                                                               debug(currentPath + " " + (ok ? "" : " not ") + " matched with " + pattern);
+                                                       if (nonDirectoryOk)
+                                                               matched.add(relativeToFullPath(base, newCurrentPath));
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+
+       protected void matchFile() {
+
+       }
+
+       /*
+        * LOW LEVEL UTILITIES
+        */
+       /**
+        * The bundles already installed. Key is location (String) , value is a
+        * {@link Bundle}
+        */
+       public Map<String, Bundle> getBundlesByLocation() {
+               Map<String, Bundle> installedBundles = new HashMap<String, Bundle>();
+               Bundle[] bundles = bundleContext.getBundles();
+               for (int i = 0; i < bundles.length; i++) {
+                       installedBundles.put(bundles[i].getLocation(), bundles[i]);
+               }
+               return installedBundles;
+       }
+
+       /**
+        * The bundles already installed. Key is symbolic name (String) , value is a
+        * {@link Bundle}
+        */
+       public Map<String, Bundle> getBundlesBySymbolicName() {
+               Map<String, Bundle> namedBundles = new HashMap<String, Bundle>();
+               Bundle[] bundles = bundleContext.getBundles();
+               for (int i = 0; i < bundles.length; i++) {
+                       namedBundles.put(bundles[i].getSymbolicName(), bundles[i]);
+               }
+               return namedBundles;
+       }
+
+       /** Creates an URL from a location */
+       protected String locationToUrl(String baseUrl, String location) {
+               return baseUrl + location;
+       }
+
+       /** Transforms a relative path in a full system path. */
+       protected String relativeToFullPath(String basePath, String relativePath) {
+               return (basePath + '/' + relativePath).replace('/', File.separatorChar);
+       }
+
+       private void refreshFramework() {
+               Bundle systemBundle = bundleContext.getBundle(0);
+               FrameworkWiring frameworkWiring = systemBundle.adapt(FrameworkWiring.class);
+               frameworkWiring.refreshBundles(null);
+       }
+
+       /**
+        * Gets a property value
+        * 
+        * @return null when defaultValue is ""
+        */
+       public String getProperty(String name, String defaultValue) {
+               String value = bundleContext.getProperty(name);
+               if (value == null)
+                       return defaultValue; // may be null
+               else
+                       return value;
+       }
+
+       public String getProperty(String name) {
+               return getProperty(name, null);
+       }
+
+       /*
+        * BEAN METHODS
+        */
+
+       public boolean getDebug() {
+               return OsgiBootUtils.debug;
+       }
+
+       // public void setDebug(boolean debug) {
+       // this.debug = debug;
+       // }
+
+       public BundleContext getBundleContext() {
+               return bundleContext;
+       }
+
+       public String getLocalCache() {
+               return localCache;
+       }
+
+       // public void setDefaultTimeout(long defaultTimeout) {
+       // this.defaultTimeout = defaultTimeout;
+       // }
+
+       // public boolean isExcludeSvn() {
+       // return excludeSvn;
+       // }
+       //
+       // public void setExcludeSvn(boolean excludeSvn) {
+       // this.excludeSvn = excludeSvn;
+       // }
+
+       /*
+        * INTERNAL CLASSES
+        */
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootConstants.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootConstants.java
new file mode 100644 (file)
index 0000000..e2d7719
--- /dev/null
@@ -0,0 +1,5 @@
+package org.argeo.osgi.boot;
+
+public interface OsgiBootConstants {
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootDiagnostics.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootDiagnostics.java
new file mode 100644 (file)
index 0000000..24a3317
--- /dev/null
@@ -0,0 +1,78 @@
+package org.argeo.osgi.boot;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.packageadmin.ExportedPackage;
+import org.osgi.service.packageadmin.PackageAdmin;
+
+@SuppressWarnings("deprecation")
+class OsgiBootDiagnostics {
+       private final BundleContext bundleContext;
+
+       public OsgiBootDiagnostics(BundleContext bundleContext) {
+               this.bundleContext = bundleContext;
+       }
+       /*
+        * DIAGNOSTICS
+        */
+       /** Check unresolved bundles */
+       protected void checkUnresolved() {
+               // Refresh
+               ServiceReference<PackageAdmin> packageAdminRef = bundleContext.getServiceReference(PackageAdmin.class);
+               PackageAdmin packageAdmin = (PackageAdmin) bundleContext.getService(packageAdminRef);
+               packageAdmin.resolveBundles(null);
+
+               Bundle[] bundles = bundleContext.getBundles();
+               List<Bundle> unresolvedBundles = new ArrayList<Bundle>();
+               for (int i = 0; i < bundles.length; i++) {
+                       int bundleState = bundles[i].getState();
+                       if (!(bundleState == Bundle.ACTIVE || bundleState == Bundle.RESOLVED || bundleState == Bundle.STARTING))
+                               unresolvedBundles.add(bundles[i]);
+               }
+
+               if (unresolvedBundles.size() != 0) {
+                       OsgiBootUtils.warn("Unresolved bundles " + unresolvedBundles);
+               }
+       }
+
+       /** List packages exported twice. */
+       public Map<String, Set<String>> findPackagesExportedTwice() {
+               ServiceReference<PackageAdmin> paSr = bundleContext.getServiceReference(PackageAdmin.class);
+               PackageAdmin packageAdmin = (PackageAdmin) bundleContext.getService(paSr);
+
+               // find packages exported twice
+               Bundle[] bundles = bundleContext.getBundles();
+               Map<String, Set<String>> exportedPackages = new TreeMap<String, Set<String>>();
+               for (int i = 0; i < bundles.length; i++) {
+                       Bundle bundle = bundles[i];
+                       ExportedPackage[] pkgs = packageAdmin.getExportedPackages(bundle);
+                       if (pkgs != null)
+                               for (int j = 0; j < pkgs.length; j++) {
+                                       String pkgName = pkgs[j].getName();
+                                       if (!exportedPackages.containsKey(pkgName)) {
+                                               exportedPackages.put(pkgName, new TreeSet<String>());
+                                       }
+                                       (exportedPackages.get(pkgName)).add(bundle.getSymbolicName() + "_" + bundle.getVersion());
+                               }
+               }
+               Map<String, Set<String>> duplicatePackages = new TreeMap<String, Set<String>>();
+               Iterator<String> it = exportedPackages.keySet().iterator();
+               while (it.hasNext()) {
+                       String pkgName = it.next().toString();
+                       Set<String> bdles = exportedPackages.get(pkgName);
+                       if (bdles.size() > 1)
+                               duplicatePackages.put(pkgName, bdles);
+               }
+               return duplicatePackages;
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootException.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootException.java
new file mode 100644 (file)
index 0000000..49c5e81
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.osgi.boot;
+
+/** OsgiBoot specific exceptions */
+public class OsgiBootException extends RuntimeException {
+       private static final long serialVersionUID = 2414011711711425353L;
+
+       public OsgiBootException() {
+       }
+
+       public OsgiBootException(String message) {
+               super(message);
+       }
+
+       public OsgiBootException(String message, Throwable e) {
+               super(message, e);
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootUtils.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootUtils.java
new file mode 100644 (file)
index 0000000..455c961
--- /dev/null
@@ -0,0 +1,129 @@
+/*\r
+ * Copyright (C) 2007-2012 Argeo GmbH\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *         http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package org.argeo.osgi.boot;\r
+\r
+import java.text.DateFormat;\r
+import java.text.SimpleDateFormat;\r
+import java.util.ArrayList;\r
+import java.util.Date;\r
+import java.util.List;\r
+import java.util.StringTokenizer;\r
+\r
+import org.osgi.framework.Bundle;\r
+\r
+/** Utilities, mostly related to logging. */\r
+public class OsgiBootUtils {\r
+       /** ISO8601 (as per log4j) and difference to UTC */\r
+       private static DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS Z");\r
+\r
+       static boolean debug = System.getProperty(OsgiBoot.PROP_ARGEO_OSGI_BOOT_DEBUG) == null ? false\r
+                       : !System.getProperty(OsgiBoot.PROP_ARGEO_OSGI_BOOT_DEBUG).trim().equals("false");\r
+\r
+       public static void info(Object obj) {\r
+               System.out.println("# OSGiBOOT      # " + dateFormat.format(new Date()) + " # " + obj);\r
+       }\r
+\r
+       public static void debug(Object obj) {\r
+               if (debug)\r
+                       System.out.println("# OSGiBOOT DBG  # " + dateFormat.format(new Date()) + " # " + obj);\r
+       }\r
+\r
+       public static void warn(Object obj) {\r
+               System.out.println("# OSGiBOOT WARN # " + dateFormat.format(new Date()) + " # " + obj);\r
+       }\r
+\r
+       public static void error(Object obj, Throwable e) {\r
+               System.err.println("# OSGiBOOT ERR  # " + dateFormat.format(new Date()) + " # " + obj);\r
+               if (e != null)\r
+                       e.printStackTrace();\r
+       }\r
+\r
+       public static boolean isDebug() {\r
+               return debug;\r
+       }\r
+\r
+       public static String stateAsString(int state) {\r
+               switch (state) {\r
+               case Bundle.UNINSTALLED:\r
+                       return "UNINSTALLED";\r
+               case Bundle.INSTALLED:\r
+                       return "INSTALLED";\r
+               case Bundle.RESOLVED:\r
+                       return "RESOLVED";\r
+               case Bundle.STARTING:\r
+                       return "STARTING";\r
+               case Bundle.ACTIVE:\r
+                       return "ACTIVE";\r
+               case Bundle.STOPPING:\r
+                       return "STOPPING";\r
+               default:\r
+                       return Integer.toString(state);\r
+               }\r
+       }\r
+\r
+       /**\r
+        * @return ==0: versions are identical, &lt;0: tested version is newer, &gt;0:\r
+        *         currentVersion is newer.\r
+        */\r
+       public static int compareVersions(String currentVersion, String testedVersion) {\r
+               List<String> cToks = new ArrayList<String>();\r
+               StringTokenizer cSt = new StringTokenizer(currentVersion, ".");\r
+               while (cSt.hasMoreTokens())\r
+                       cToks.add(cSt.nextToken());\r
+               List<String> tToks = new ArrayList<String>();\r
+               StringTokenizer tSt = new StringTokenizer(currentVersion, ".");\r
+               while (tSt.hasMoreTokens())\r
+                       tToks.add(tSt.nextToken());\r
+\r
+               int comp = 0;\r
+               comp: for (int i = 0; i < cToks.size(); i++) {\r
+                       if (tToks.size() <= i) {\r
+                               // equals until then, tested shorter\r
+                               comp = 1;\r
+                               break comp;\r
+                       }\r
+\r
+                       String c = (String) cToks.get(i);\r
+                       String t = (String) tToks.get(i);\r
+\r
+                       try {\r
+                               int cInt = Integer.parseInt(c);\r
+                               int tInt = Integer.parseInt(t);\r
+                               if (cInt == tInt)\r
+                                       continue comp;\r
+                               else {\r
+                                       comp = (cInt - tInt);\r
+                                       break comp;\r
+                               }\r
+                       } catch (NumberFormatException e) {\r
+                               if (c.equals(t))\r
+                                       continue comp;\r
+                               else {\r
+                                       comp = c.compareTo(t);\r
+                                       break comp;\r
+                               }\r
+                       }\r
+               }\r
+\r
+               if (comp == 0 && tToks.size() > cToks.size()) {\r
+                       // equals until then, current shorter\r
+                       comp = -1;\r
+               }\r
+\r
+               return comp;\r
+       }\r
+\r
+}\r
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBuilder.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBuilder.java
new file mode 100644 (file)
index 0000000..8c460e1
--- /dev/null
@@ -0,0 +1,327 @@
+package org.argeo.osgi.boot;
+
+import java.lang.reflect.Method;
+import java.net.URI;
+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.Properties;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.eclipse.osgi.launch.EquinoxFactory;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+import org.osgi.util.tracker.BundleTracker;
+import org.osgi.util.tracker.ServiceTracker;
+
+/** OSGi builder, focusing on ease of use for scripting. */
+public class OsgiBuilder {
+       private final static String PROP_HTTP_PORT = "org.osgi.service.http.port";
+       private final static String PROP_HTTPS_PORT = "org.osgi.service.https.port";
+       private final static String PROP_OSGI_CLEAN = "osgi.clean";
+
+       private Map<Integer, StartLevel> startLevels = new TreeMap<>();
+       private List<String> distributionBundles = new ArrayList<>();
+
+       private Map<String, String> configuration = new HashMap<String, String>();
+       private Framework framework;
+       private String baseUrl = null;
+
+       public OsgiBuilder() {
+               // configuration.put("osgi.clean", "true");
+               configuration.put(OsgiBoot.CONFIGURATION_AREA_PROP, System.getProperty(OsgiBoot.CONFIGURATION_AREA_PROP));
+               configuration.put(OsgiBoot.INSTANCE_AREA_PROP, System.getProperty(OsgiBoot.INSTANCE_AREA_PROP));
+               configuration.put(PROP_OSGI_CLEAN, System.getProperty(PROP_OSGI_CLEAN));
+       }
+
+       public Framework launch() {
+               // start OSGi
+               FrameworkFactory frameworkFactory = new EquinoxFactory();
+               framework = frameworkFactory.newFramework(configuration);
+               try {
+                       framework.start();
+               } catch (BundleException e) {
+                       throw new OsgiBootException("Cannot start OSGi framework", e);
+               }
+
+               BundleContext bc = framework.getBundleContext();
+               String osgiData = bc.getProperty(OsgiBoot.INSTANCE_AREA_PROP);
+               // String osgiConf = bc.getProperty(OsgiBoot.CONFIGURATION_AREA_PROP);
+               String osgiConf = framework.getDataFile("").getAbsolutePath();
+               if (OsgiBootUtils.isDebug())
+                       OsgiBootUtils.debug("OSGi starting - data: " + osgiData + " conf: " + osgiConf);
+
+               OsgiBoot osgiBoot = new OsgiBoot(framework.getBundleContext());
+               if (distributionBundles.isEmpty()) {
+                       osgiBoot.getProvisioningManager().install(null);
+               } else {
+                       // install bundles
+                       for (String distributionBundle : distributionBundles) {
+                               List<String> bundleUrls = osgiBoot.getDistributionUrls(distributionBundle, baseUrl);
+                               osgiBoot.installUrls(bundleUrls);
+                       }
+               }
+               // start bundles
+               osgiBoot.startBundles(startLevelsToProperties());
+
+               // if (OsgiBootUtils.isDebug())
+               // for (Bundle bundle : bc.getBundles()) {
+               // OsgiBootUtils.debug(bundle.getLocation());
+               // }
+               return framework;
+       }
+
+       public OsgiBuilder conf(String key, String value) {
+               checkNotLaunched();
+               configuration.put(key, value);
+               return this;
+       }
+
+       public OsgiBuilder install(String uri) {
+               // TODO dynamic install
+               checkNotLaunched();
+               if (!distributionBundles.contains(uri))
+                       distributionBundles.add(uri);
+               return this;
+       }
+
+       public OsgiBuilder start(int startLevel, String bundle) {
+               // TODO dynamic start
+               checkNotLaunched();
+               StartLevel sl;
+               if (!startLevels.containsKey(startLevel))
+                       startLevels.put(startLevel, new StartLevel());
+               sl = startLevels.get(startLevel);
+               sl.add(bundle);
+               return this;
+       }
+
+       public OsgiBuilder waitForServlet(String base) {
+               service("(&(objectClass=javax.servlet.Servlet)(osgi.http.whiteboard.servlet.pattern=" + base + "))");
+               return this;
+       }
+
+       public OsgiBuilder waitForBundle(String bundles) {
+               List<String> lst = new ArrayList<>();
+               Collections.addAll(lst, bundles.split(","));
+               BundleTracker<Object> bt = new BundleTracker<Object>(getBc(), Bundle.ACTIVE, null) {
+
+                       @Override
+                       public Object addingBundle(Bundle bundle, BundleEvent event) {
+                               if (lst.contains(bundle.getSymbolicName())) {
+                                       return bundle.getSymbolicName();
+                               } else {
+                                       return null;
+                               }
+                       }
+               };
+               bt.open();
+               while (bt.getTrackingCount() != lst.size()) {
+                       try {
+                               Thread.sleep(500l);
+                       } catch (InterruptedException e) {
+                               break;
+                       }
+               }
+               bt.close();
+               return this;
+
+       }
+
+       public OsgiBuilder main(String clssUri, String[] args) {
+
+               // waitForBundle(bundleSymbolicName);
+               try {
+                       URI uri = new URI(clssUri);
+                       if (!"bundleclass".equals(uri.getScheme()))
+                               throw new IllegalArgumentException("Unsupported scheme for " + clssUri);
+                       String bundleSymbolicName = uri.getHost();
+                       String clss = uri.getPath().substring(1);
+                       Bundle bundle = null;
+                       for (Bundle b : getBc().getBundles()) {
+                               if (bundleSymbolicName.equals(b.getSymbolicName())) {
+                                       bundle = b;
+                                       break;
+                               }
+                       }
+                       if (bundle == null)
+                               throw new OsgiBootException("Bundle " + bundleSymbolicName + " not found");
+                       Class<?> c = bundle.loadClass(clss);
+                       Object[] mainArgs = { args };
+                       Method mainMethod = c.getMethod("main", String[].class);
+                       mainMethod.invoke(null, mainArgs);
+               } catch (Throwable e) {
+                       throw new OsgiBootException("Cannot execute " + clssUri, e);
+               }
+               return this;
+       }
+
+       public Object service(String service) {
+               return service(service, 0);
+       }
+
+       public Object service(String service, long timeout) {
+               ServiceTracker<Object, Object> st;
+               if (service.contains("(")) {
+                       try {
+                               st = new ServiceTracker<>(getBc(), FrameworkUtil.createFilter(service), null);
+                       } catch (InvalidSyntaxException e) {
+                               throw new IllegalArgumentException("Badly formatted filter", e);
+                       }
+               } else {
+                       st = new ServiceTracker<>(getBc(), service, null);
+               }
+               st.open();
+               try {
+                       return st.waitForService(timeout);
+               } catch (InterruptedException e) {
+                       OsgiBootUtils.error("Interrupted", e);
+                       return null;
+               } finally {
+                       st.close();
+               }
+
+       }
+
+       public void shutdown() {
+               checkLaunched();
+               try {
+                       framework.stop();
+               } catch (BundleException e) {
+                       e.printStackTrace();
+                       System.exit(1);
+               }
+               try {
+                       framework.waitForStop(10 * 60 * 1000);
+               } catch (InterruptedException e) {
+                       e.printStackTrace();
+                       System.exit(1);
+               }
+               System.exit(0);
+       }
+
+       public void setHttpPort(Integer port) {
+               checkNotLaunched();
+               configuration.put(PROP_HTTP_PORT, Integer.toString(port));
+       }
+
+       public void setHttpsPort(Integer port) {
+               checkNotLaunched();
+               configuration.put(PROP_HTTPS_PORT, Integer.toString(port));
+       }
+
+       public void setClean(boolean clean) {
+               checkNotLaunched();
+               configuration.put(PROP_OSGI_CLEAN, Boolean.toString(clean));
+       }
+
+       public Integer getHttpPort() {
+               if (!isLaunched()) {
+                       if (configuration.containsKey(PROP_HTTP_PORT))
+                               return Integer.parseInt(configuration.get(PROP_HTTP_PORT));
+                       else
+                               return -1;
+               } else {
+                       // TODO wait for service?
+                       ServiceReference<?> sr = getBc().getServiceReference("org.osgi.service.http.HttpService");
+                       if (sr == null)
+                               return -1;
+                       Object port = sr.getProperty("http.port");
+                       if (port == null)
+                               return -1;
+                       return Integer.parseInt(port.toString());
+               }
+       }
+
+       public Integer getHttpsPort() {
+               if (!isLaunched()) {
+                       if (configuration.containsKey(PROP_HTTPS_PORT))
+                               return Integer.parseInt(configuration.get(PROP_HTTPS_PORT));
+                       else
+                               return -1;
+               } else {
+                       // TODO wait for service?
+                       ServiceReference<?> sr = getBc().getServiceReference("org.osgi.service.http.HttpService");
+                       if (sr == null)
+                               return -1;
+                       Object port = sr.getProperty("https.port");
+                       if (port == null)
+                               return -1;
+                       return Integer.parseInt(port.toString());
+               }
+       }
+
+       public Object spring(String bundle) {
+               return service("(&(Bundle-SymbolicName=" + bundle + ")"
+                               + "(objectClass=org.springframework.context.ApplicationContext))");
+       }
+
+       //
+       // BEAN
+       //
+
+       public BundleContext getBc() {
+               checkLaunched();
+               return framework.getBundleContext();
+       }
+
+       public void setBaseUrl(String baseUrl) {
+               this.baseUrl = baseUrl;
+       }
+
+       //
+       // UTILITIES
+       //
+       private Properties startLevelsToProperties() {
+               Properties properties = new Properties();
+               for (Integer startLevel : startLevels.keySet()) {
+                       String property = OsgiBoot.PROP_ARGEO_OSGI_START + "." + startLevel;
+                       StringBuilder value = new StringBuilder();
+                       for (String bundle : startLevels.get(startLevel).getBundles()) {
+                               value.append(bundle);
+                               value.append(',');
+                       }
+                       // TODO remove trailing comma
+                       properties.put(property, value.toString());
+               }
+               return properties;
+       }
+
+       private void checkLaunched() {
+               if (!isLaunched())
+                       throw new OsgiBootException("OSGi runtime is not launched");
+       }
+
+       private void checkNotLaunched() {
+               if (isLaunched())
+                       throw new OsgiBootException("OSGi runtime already launched");
+       }
+
+       private boolean isLaunched() {
+               return framework != null;
+       }
+
+       private static class StartLevel {
+               private Set<String> bundles = new HashSet<>();
+
+               public void add(String bundle) {
+                       String[] b = bundle.split(",");
+                       Collections.addAll(bundles, b);
+               }
+
+               public Set<String> getBundles() {
+                       return bundles;
+               }
+       }
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Branch.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Branch.java
new file mode 100644 (file)
index 0000000..ae715ec
--- /dev/null
@@ -0,0 +1,80 @@
+package org.argeo.osgi.boot.a2;
+
+import java.util.Collections;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.argeo.osgi.boot.OsgiBootUtils;
+import org.osgi.framework.Version;
+
+class A2Branch implements Comparable<A2Branch> {
+       private final A2Component component;
+       private final String id;
+
+       final SortedMap<Version, A2Module> modules = Collections.synchronizedSortedMap(new TreeMap<>());
+
+       A2Branch(A2Component component, String id) {
+               this.component = component;
+               this.id = id;
+               component.branches.put(id, this);
+       }
+
+       A2Module getOrAddModule(Version version, Object locator) {
+               if (modules.containsKey(version)) {
+                       A2Module res = modules.get(version);
+                       if (OsgiBootUtils.isDebug() && !res.getLocator().equals(locator)) {
+                               OsgiBootUtils.debug("Inconsistent locator " + locator + " (registered: " + res.getLocator() + ")");
+                       }
+                       return res;
+               } else
+                       return new A2Module(this, version, locator);
+       }
+
+       A2Module last() {
+               return modules.get(modules.lastKey());
+       }
+
+       A2Module first() {
+               return modules.get(modules.firstKey());
+       }
+
+       A2Component getComponent() {
+               return component;
+       }
+
+       String getId() {
+               return id;
+       }
+
+       @Override
+       public int compareTo(A2Branch o) {
+               return id.compareTo(id);
+       }
+
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof A2Branch) {
+                       A2Branch o = (A2Branch) obj;
+                       return component.equals(o.component) && id.equals(o.id);
+               } else
+                       return false;
+       }
+
+       @Override
+       public String toString() {
+               return getCoordinates();
+       }
+
+       public String getCoordinates() {
+               return component + ":" + id;
+       }
+
+       static String versionToBranchId(Version version) {
+               return version.getMajor() + "." + version.getMinor();
+       }
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Component.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Component.java
new file mode 100644 (file)
index 0000000..5d8dc87
--- /dev/null
@@ -0,0 +1,95 @@
+package org.argeo.osgi.boot.a2;
+
+import java.util.Collections;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.osgi.framework.Version;
+
+class A2Component implements Comparable<A2Component> {
+       private final A2Contribution contribution;
+       private final String id;
+
+       final SortedMap<String, A2Branch> branches = Collections.synchronizedSortedMap(new TreeMap<>());
+
+       public A2Component(A2Contribution contribution, String id) {
+               this.contribution = contribution;
+               this.id = id;
+               contribution.components.put(id, this);
+       }
+
+       A2Branch getOrAddBranch(String branchId) {
+               if (branches.containsKey(branchId))
+                       return branches.get(branchId);
+               else
+                       return new A2Branch(this, branchId);
+       }
+
+       A2Module getOrAddModule(Version version, Object locator) {
+               A2Branch branch = getOrAddBranch(A2Branch.versionToBranchId(version));
+               A2Module module = branch.getOrAddModule(version, locator);
+               return module;
+       }
+
+       A2Branch last() {
+               return branches.get(branches.lastKey());
+       }
+
+       A2Contribution getContribution() {
+               return contribution;
+       }
+
+       String getId() {
+               return id;
+       }
+
+       @Override
+       public int compareTo(A2Component o) {
+               return id.compareTo(o.id);
+       }
+
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof A2Component) {
+                       A2Component o = (A2Component) obj;
+                       return contribution.equals(o.contribution) && id.equals(o.id);
+               } else
+                       return false;
+       }
+
+       @Override
+       public String toString() {
+               return contribution.getId() + ":" + id;
+       }
+
+       void asTree(String prefix, StringBuffer buf) {
+               if (prefix == null)
+                       prefix = "";
+               A2Branch lastBranch = last();
+               SortedMap<String, A2Branch> displayMap =  new TreeMap<>(Collections.reverseOrder());
+               displayMap.putAll(branches);
+               for (String branchId : displayMap.keySet()) {
+                       A2Branch branch = displayMap.get(branchId);
+                       if (!lastBranch.equals(branch)) {
+                               buf.append('\n');
+                               buf.append(prefix);
+                       } else {
+                               buf.append(" -");
+                       }
+                       buf.append(prefix);
+                       buf.append(branchId);
+                       A2Module first = branch.first();
+                       A2Module last = branch.last();
+                       buf.append(" (").append(last.getVersion());
+                       if (!first.equals(last))
+                               buf.append(" ... ").append(first.getVersion());
+                       buf.append(')');
+               }
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Contribution.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Contribution.java
new file mode 100644 (file)
index 0000000..9e6ca30
--- /dev/null
@@ -0,0 +1,74 @@
+package org.argeo.osgi.boot.a2;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+
+class A2Contribution implements Comparable<A2Contribution> {
+       final static String BOOT = "boot";
+       final static String RUNTIME = "runtime";
+
+       private final ProvisioningSource source;
+       private final String id;
+
+       final Map<String, A2Component> components = Collections.synchronizedSortedMap(new TreeMap<>());
+
+       public A2Contribution(ProvisioningSource context, String id) {
+               this.source = context;
+               this.id = id;
+               if (context != null)
+                       context.contributions.put(id, this);
+       }
+
+       A2Component getOrAddComponent(String componentId) {
+               if (components.containsKey(componentId))
+                       return components.get(componentId);
+               else
+                       return new A2Component(this, componentId);
+       }
+
+       public ProvisioningSource getSource() {
+               return source;
+       }
+
+       public String getId() {
+               return id;
+       }
+
+       @Override
+       public int compareTo(A2Contribution o) {
+               return id.compareTo(o.id);
+       }
+
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof A2Contribution) {
+                       A2Contribution o = (A2Contribution) obj;
+                       return id.equals(o.id);
+               } else
+                       return false;
+       }
+
+       @Override
+       public String toString() {
+               return id;
+       }
+
+       void asTree(String prefix, StringBuffer buf) {
+               if (prefix == null)
+                       prefix = "";
+               for (String componentId : components.keySet()) {
+                       buf.append(prefix);
+                       buf.append(componentId);
+                       A2Component component = components.get(componentId);
+                       component.asTree(prefix, buf);
+                       buf.append('\n');
+               }
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Module.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Module.java
new file mode 100644 (file)
index 0000000..1108cd7
--- /dev/null
@@ -0,0 +1,58 @@
+package org.argeo.osgi.boot.a2;
+
+import org.osgi.framework.Version;
+
+class A2Module implements Comparable<A2Module> {
+       private final A2Branch branch;
+       private final Version version;
+       private final Object locator;
+
+       public A2Module(A2Branch branch, Version version, Object locator) {
+               this.branch = branch;
+               this.version = version;
+               this.locator = locator;
+               branch.modules.put(version, this);
+       }
+
+       A2Branch getBranch() {
+               return branch;
+       }
+
+       Version getVersion() {
+               return version;
+       }
+
+       Object getLocator() {
+               return locator;
+       }
+
+       @Override
+       public int compareTo(A2Module o) {
+               return version.compareTo(o.version);
+       }
+
+       @Override
+       public int hashCode() {
+               return version.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof A2Module) {
+                       A2Module o = (A2Module) obj;
+                       return branch.equals(o.branch) && version.equals(o.version);
+               } else
+                       return false;
+       }
+
+       @Override
+       public String toString() {
+               return getCoordinates();
+       }
+
+       public String getCoordinates() {
+               return branch.getComponent() + ":" + version;
+       }
+
+       
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsA2Source.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsA2Source.java
new file mode 100644 (file)
index 0000000..b9f9193
--- /dev/null
@@ -0,0 +1,82 @@
+package org.argeo.osgi.boot.a2;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.argeo.osgi.boot.OsgiBootUtils;
+import org.osgi.framework.Version;
+
+public class FsA2Source extends ProvisioningSource {
+       private final Path base;
+
+       public FsA2Source(Path base) {
+               super();
+               this.base = base;
+       }
+
+       void load() throws IOException {
+               DirectoryStream<Path> contributionPaths = Files.newDirectoryStream(base);
+               SortedSet<A2Contribution> contributions = new TreeSet<>();
+               contributions: for (Path contributionPath : contributionPaths) {
+                       if (Files.isDirectory(contributionPath)) {
+                               String contributionId = contributionPath.getFileName().toString();
+                               if (A2Contribution.BOOT.equals(contributionId))// skip boot
+                                       continue contributions;
+                               A2Contribution contribution = new A2Contribution(this, contributionId);
+                               contributions.add(contribution);
+                       }
+               }
+
+               for (A2Contribution contribution : contributions) {
+                       DirectoryStream<Path> modulePaths = Files.newDirectoryStream(base.resolve(contribution.getId()));
+                       modules: for (Path modulePath : modulePaths) {
+                               if (!Files.isDirectory(modulePath)) {
+                                       // OsgiBootUtils.debug("Registering " + modulePath);
+                                       String moduleFileName = modulePath.getFileName().toString();
+                                       int lastDot = moduleFileName.lastIndexOf('.');
+                                       String ext = moduleFileName.substring(lastDot + 1);
+                                       if (!"jar".equals(ext))
+                                               continue modules;
+                                       String moduleName = moduleFileName.substring(0, lastDot);
+                                       int firstDash = moduleName.indexOf('-');
+                                       String versionStr = moduleName.substring(firstDash + 1);
+                                       String componentName = moduleName.substring(0, firstDash);
+                                       // if(versionStr.endsWith("-SNAPSHOT")) {
+                                       // versionStr = readVersionFromModule(modulePath);
+                                       // }
+                                       Version version;
+                                       try {
+                                               version = new Version(versionStr);
+                                       } catch (Exception e) {
+                                               versionStr = readVersionFromModule(modulePath);
+                                               version = new Version(versionStr);
+                                               // OsgiBootUtils.debug("Ignore " + modulePath + " (" + e.getMessage() + ")");
+                                               // continue modules;
+                                       }
+                                       A2Component component = contribution.getOrAddComponent(componentName);
+                                       A2Module module = component.getOrAddModule(version, modulePath);
+                                       if (OsgiBootUtils.isDebug())
+                                               OsgiBootUtils.debug("Registered " + module);
+                               }
+                       }
+               }
+
+       }
+
+       public static void main(String[] args) {
+               try {
+                       FsA2Source context = new FsA2Source(Paths.get(
+                                       "/home/mbaudier/dev/git/apache2/argeo-commons/dist/argeo-node/target/argeo-node-2.1.74-SNAPSHOT/argeo-node/share/osgi"));
+                       context.load();
+                       context.asTree();
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsM2Source.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsM2Source.java
new file mode 100644 (file)
index 0000000..ecf8771
--- /dev/null
@@ -0,0 +1,65 @@
+package org.argeo.osgi.boot.a2;
+
+import java.io.File;
+import java.io.IOException;
+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 org.argeo.osgi.boot.OsgiBootUtils;
+import org.osgi.framework.Version;
+
+public class FsM2Source extends ProvisioningSource {
+       private final Path base;
+
+       public FsM2Source(Path base) {
+               super();
+               this.base = base;
+       }
+
+       void load() throws IOException {
+               Files.walkFileTree(base, new ArtifactFileVisitor());
+       }
+
+       class ArtifactFileVisitor extends SimpleFileVisitor<Path> {
+
+               @Override
+               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                       // OsgiBootUtils.debug("Processing " + file);
+                       if (file.toString().endsWith(".jar")) {
+                               Version version;
+                               try {
+                                       version = new Version(readVersionFromModule(file));
+                               } catch (Exception e) {
+                                       // ignore non OSGi
+                                       return FileVisitResult.CONTINUE;
+                               }
+                               String moduleName = readSymbolicNameFromModule(file);
+                               Path groupPath = file.getParent().getParent().getParent();
+                               Path relGroupPath = base.relativize(groupPath);
+                               String contributionName = relGroupPath.toString().replace(File.separatorChar, '.');
+                               A2Contribution contribution = getOrAddContribution(contributionName);
+                               A2Component component = contribution.getOrAddComponent(moduleName);
+                               A2Module module = component.getOrAddModule(version, file);
+                               if (OsgiBootUtils.isDebug())
+                                       OsgiBootUtils.debug("Registered " + module);
+                       }
+                       return super.visitFile(file, attrs);
+               }
+
+       }
+
+       public static void main(String[] args) {
+               try {
+                       FsM2Source context = new FsM2Source(Paths.get("/home/mbaudier/.m2/repository"));
+                       context.load();
+                       context.asTree();
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/OsgiContext.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/OsgiContext.java
new file mode 100644 (file)
index 0000000..a3ddf55
--- /dev/null
@@ -0,0 +1,38 @@
+package org.argeo.osgi.boot.a2;
+
+import org.argeo.osgi.boot.OsgiBootUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.Version;
+
+class OsgiContext extends ProvisioningSource {
+       private final BundleContext bc;
+
+       public OsgiContext(BundleContext bc) {
+               super();
+               this.bc = bc;
+       }
+
+       public OsgiContext() {
+               Bundle bundle = FrameworkUtil.getBundle(OsgiContext.class);
+               if (bundle == null)
+                       throw new IllegalArgumentException(
+                                       "OSGi Boot bundle must be started or a bundle context must be specified");
+               this.bc = bundle.getBundleContext();
+       }
+
+       void load() {
+               A2Contribution runtimeContribution = new A2Contribution(this, A2Contribution.RUNTIME);
+               for (Bundle bundle : bc.getBundles()) {
+                       // OsgiBootUtils.debug(bundle.getDataFile("/"));
+                       String componentId = bundle.getSymbolicName();
+                       Version version = bundle.getVersion();
+                       A2Component component = runtimeContribution.getOrAddComponent(componentId);
+                       A2Module module = component.getOrAddModule(version, bundle);
+                       if (OsgiBootUtils.isDebug())
+                               OsgiBootUtils.debug("Registered " + module + " (location id: " + bundle.getLocation() + ")");
+               }
+
+       }
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningManager.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningManager.java
new file mode 100644 (file)
index 0000000..e7a2970
--- /dev/null
@@ -0,0 +1,201 @@
+package org.argeo.osgi.boot.a2;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+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 org.argeo.osgi.boot.OsgiBootException;
+import org.argeo.osgi.boot.OsgiBootUtils;
+import org.eclipse.osgi.launch.EquinoxFactory;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+import org.osgi.framework.wiring.FrameworkWiring;
+
+public class ProvisioningManager {
+       BundleContext bc;
+       OsgiContext osgiContext;
+       List<ProvisioningSource> sources = Collections.synchronizedList(new ArrayList<>());
+
+       public ProvisioningManager(BundleContext bc) {
+               this.bc = bc;
+               osgiContext = new OsgiContext(bc);
+               osgiContext.load();
+       }
+
+       void addSource(ProvisioningSource context) {
+               sources.add(context);
+       }
+
+       void installWholeSource(ProvisioningSource context) {
+               Set<Bundle> updatedBundles = new HashSet<>();
+               for (A2Contribution contribution : context.contributions.values()) {
+                       for (A2Component component : contribution.components.values()) {
+                               A2Module module = component.last().last();
+                               Bundle bundle = installOrUpdate(module);
+                               if (bundle != null)
+                                       updatedBundles.add(bundle);
+                       }
+               }
+               FrameworkWiring frameworkWiring = bc.getBundle(0).adapt(FrameworkWiring.class);
+               frameworkWiring.refreshBundles(updatedBundles);
+       }
+
+       public void registerSource(String uri) {
+               try {
+                       URI u = new URI(uri);
+                       if ("a2".equals(u.getScheme())) {
+                               if (u.getHost() == null || "".equals(u.getHost())) {
+                                       Path base = Paths.get(u.getPath());
+                                       FsA2Source source = new FsA2Source(base);
+                                       source.load();
+                                       addSource(source);
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new OsgiBootException("Cannot add source " + uri, e);
+               }
+       }
+
+       public boolean registerDefaultSource() {
+               String frameworkLocation = bc.getProperty("osgi.framework");
+               try {
+                       URI frameworkLocationUri = new URI(frameworkLocation);
+                       if ("file".equals(frameworkLocationUri.getScheme())) {
+                               Path frameworkPath = Paths.get(frameworkLocationUri);
+                               if (frameworkPath.getParent().getFileName().toString().equals(A2Contribution.BOOT)) {
+                                       Path base = frameworkPath.getParent().getParent();
+                                       URI baseUri = new URI("a2", null, null, 0, base.toString(), null, null);
+                                       registerSource(baseUri.toString());
+                                       OsgiBootUtils.info("Registered " + baseUri + " as default source");
+                                       return true;
+                               }
+                       }
+               } catch (Exception e) {
+                       OsgiBootUtils.error("Cannot register default source based on framework location " + frameworkLocation, e);
+               }
+               return false;
+       }
+
+       public void install(String spec) {
+               if (spec == null) {
+                       for (ProvisioningSource source : sources) {
+                               installWholeSource(source);
+                       }
+               }
+       }
+
+       /** @return the new/updated bundle, or null if nothign was done. */
+       Bundle installOrUpdate(A2Module module) {
+               try {
+                       ProvisioningSource moduleSource = module.getBranch().getComponent().getContribution().getSource();
+                       Version moduleVersion = module.getVersion();
+                       A2Branch osgiBranch = osgiContext.findBranch(module.getBranch().getComponent().getId(), moduleVersion);
+                       if (osgiBranch == null) {
+                               Bundle bundle = bc.installBundle(module.getBranch().getCoordinates(),
+                                               moduleSource.newInputStream(module.getLocator()));
+                               if (OsgiBootUtils.isDebug())
+                                       OsgiBootUtils.debug("Installed bundle " + bundle.getLocation() + " with version " + moduleVersion);
+                               return bundle;
+                       } else {
+                               A2Module lastOsgiModule = osgiBranch.last();
+                               int compare = moduleVersion.compareTo(lastOsgiModule.getVersion());
+                               if (compare > 0) {// update
+                                       Bundle bundle = (Bundle) lastOsgiModule.getLocator();
+                                       bundle.update(moduleSource.newInputStream(module.getLocator()));
+                                       OsgiBootUtils.info("Updated bundle " + bundle.getLocation() + " to version " + moduleVersion);
+                                       return bundle;
+                               }
+                       }
+               } catch (Exception e) {
+                       OsgiBootUtils.error("Could not install module " + module, e);
+               }
+               return null;
+       }
+
+       public Collection<Bundle> update() {
+               boolean fragmentsUpdated = false;
+               Set<Bundle> updatedBundles = new HashSet<>();
+               bundles: for (Bundle bundle : bc.getBundles()) {
+                       for (ProvisioningSource source : sources) {
+                               String componentId = bundle.getSymbolicName();
+                               Version version = bundle.getVersion();
+                               A2Branch branch = source.findBranch(componentId, version);
+                               if (branch == null)
+                                       continue bundles;
+                               A2Module module = branch.last();
+                               Version moduleVersion = module.getVersion();
+                               int compare = moduleVersion.compareTo(version);
+                               if (compare > 0) {// update
+                                       try (InputStream in = source.newInputStream(module.getLocator())) {
+                                               bundle.update(in);
+                                               String fragmentHost = bundle.getHeaders().get(Constants.FRAGMENT_HOST);
+                                               if (fragmentHost != null)
+                                                       fragmentsUpdated = true;
+                                               OsgiBootUtils.info("Updated bundle " + bundle.getLocation() + " to version " + moduleVersion);
+                                               updatedBundles.add(bundle);
+                                       } catch (Exception e) {
+                                               OsgiBootUtils.error("Cannot update with module " + module, e);
+                                       }
+                               }
+                       }
+               }
+               FrameworkWiring frameworkWiring = bc.getBundle(0).adapt(FrameworkWiring.class);
+               if (fragmentsUpdated)// refresh all
+                       frameworkWiring.refreshBundles(null);
+               else
+                       frameworkWiring.refreshBundles(updatedBundles);
+               return updatedBundles;
+       }
+
+       private static Framework launch() {
+               // start OSGi
+               FrameworkFactory frameworkFactory = new EquinoxFactory();
+               Map<String, String> configuration = new HashMap<>();
+               configuration.put("osgi.console", "2323");
+               Framework framework = frameworkFactory.newFramework(configuration);
+               try {
+                       framework.start();
+               } catch (BundleException e) {
+                       throw new OsgiBootException("Cannot start OSGi framework", e);
+               }
+               return framework;
+       }
+
+       public static void main(String[] args) {
+               Framework framework = launch();
+               try {
+                       ProvisioningManager pm = new ProvisioningManager(framework.getBundleContext());
+                       FsA2Source context = new FsA2Source(Paths.get(
+                                       "/home/mbaudier/dev/git/apache2/argeo-commons/dist/argeo-node/target/argeo-node-2.1.74-SNAPSHOT/argeo-node/share/osgi"));
+                       context.load();
+                       if (framework.getBundleContext().getBundles().length == 1) {// initial
+                               pm.install(null);
+                       } else {
+                               pm.update();
+                       }
+               } catch (Exception e) {
+                       e.printStackTrace();
+               } finally {
+                       try {
+                               // framework.stop();
+                       } catch (Exception e) {
+                               e.printStackTrace();
+                       }
+               }
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningSource.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningSource.java
new file mode 100644 (file)
index 0000000..cb122e4
--- /dev/null
@@ -0,0 +1,106 @@
+package org.argeo.osgi.boot.a2;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+import org.argeo.osgi.boot.OsgiBootException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+
+abstract class ProvisioningSource {
+       final Map<String, A2Contribution> contributions = Collections.synchronizedSortedMap(new TreeMap<>());
+
+       A2Contribution getOrAddContribution(String contributionId) {
+               if (contributions.containsKey(contributionId))
+                       return contributions.get(contributionId);
+               else
+                       return new A2Contribution(this, contributionId);
+       }
+
+       void asTree(String prefix, StringBuffer buf) {
+               if (prefix == null)
+                       prefix = "";
+               for (String contributionId : contributions.keySet()) {
+                       buf.append(prefix);
+                       buf.append(contributionId);
+                       buf.append('\n');
+                       A2Contribution contribution = contributions.get(contributionId);
+                       contribution.asTree(prefix + " ", buf);
+               }
+       }
+
+       void asTree() {
+               StringBuffer buf = new StringBuffer();
+               asTree("", buf);
+               System.out.println(buf);
+       }
+
+       A2Component findComponent(String componentId) {
+               SortedMap<A2Contribution, A2Component> res = new TreeMap<>();
+               for (A2Contribution contribution : contributions.values()) {
+                       components: for (String componentIdKey : contribution.components.keySet()) {
+                               if (componentId.equals(componentIdKey)) {
+                                       res.put(contribution, contribution.components.get(componentIdKey));
+                                       break components;
+                               }
+                       }
+               }
+               if (res.size() == 0)
+                       return null;
+               // TODO explicit contribution priorities
+               return res.get(res.lastKey());
+
+       }
+
+       A2Branch findBranch(String componentId, Version version) {
+               A2Component component = findComponent(componentId);
+               if (component == null)
+                       return null;
+               String branchId = version.getMajor() + "." + version.getMinor();
+               if (!component.branches.containsKey(branchId))
+                       return null;
+               return component.branches.get(branchId);
+       }
+
+       protected String readVersionFromModule(Path modulePath) {
+               try (JarInputStream in = new JarInputStream(newInputStream(modulePath))) {
+                       Manifest manifest = in.getManifest();
+                       String versionStr = manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+                       return versionStr;
+               } catch (IOException e) {
+                       throw new OsgiBootException("Cannot read manifest from " + modulePath, e);
+               }
+       }
+
+       protected String readSymbolicNameFromModule(Path modulePath) {
+               try (JarInputStream in = new JarInputStream(newInputStream(modulePath))) {
+                       Manifest manifest = in.getManifest();
+                       String symbolicName = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+                       int semiColIndex = symbolicName.indexOf(';');
+                       if (semiColIndex >= 0)
+                               symbolicName = symbolicName.substring(0, semiColIndex);
+                       return symbolicName;
+               } catch (IOException e) {
+                       throw new OsgiBootException("Cannot read manifest from " + modulePath, e);
+               }
+       }
+
+       InputStream newInputStream(Object locator) throws IOException {
+               if (locator instanceof Path) {
+                       return Files.newInputStream((Path) locator);
+               } else if (locator instanceof URL) {
+                       return ((URL) locator).openStream();
+               } else {
+                       throw new IllegalArgumentException("Unsupported module locator type " + locator.getClass());
+               }
+       }
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/AntPathMatcher.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/AntPathMatcher.java
new file mode 100644 (file)
index 0000000..e3fc6c2
--- /dev/null
@@ -0,0 +1,411 @@
+/*\r
+ * Copyright 2002-2007 the original author or authors.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.argeo.osgi.boot.internal.springutil;\r
+\r
+/**\r
+ * PathMatcher implementation for Ant-style path patterns.\r
+ * Examples are provided below.\r
+ *\r
+ * <p>Part of this mapping code has been kindly borrowed from\r
+ * <a href="http://ant.apache.org">Apache Ant</a>.\r
+ *\r
+ * <p>The mapping matches URLs using the following rules:<br>\r
+ * <ul>\r
+ * <li>? matches one character</li>\r
+ * <li>* matches zero or more characters</li>\r
+ * <li>** matches zero or more 'directories' in a path</li>\r
+ * </ul>\r
+ *\r
+ * <p>Some examples:<br>\r
+ * <ul>\r
+ * <li><code>com/t?st.jsp</code> - matches <code>com/test.jsp</code> but also\r
+ * <code>com/tast.jsp</code> or <code>com/txst.jsp</code></li>\r
+ * <li><code>com/*.jsp</code> - matches all <code>.jsp</code> files in the\r
+ * <code>com</code> directory</li>\r
+ * <li><code>com/&#42;&#42;/test.jsp</code> - matches all <code>test.jsp</code>\r
+ * files underneath the <code>com</code> path</li>\r
+ * <li><code>org/springframework/&#42;&#42;/*.jsp</code> - matches all <code>.jsp</code>\r
+ * files underneath the <code>org/springframework</code> path</li>\r
+ * <li><code>org/&#42;&#42;/servlet/bla.jsp</code> - matches\r
+ * <code>org/springframework/servlet/bla.jsp</code> but also\r
+ * <code>org/springframework/testing/servlet/bla.jsp</code> and\r
+ * <code>org/servlet/bla.jsp</code></li>\r
+ * </ul>\r
+ *\r
+ * @author Alef Arendsen\r
+ * @author Juergen Hoeller\r
+ * @author Rob Harrop\r
+ * @since 16.07.2003\r
+ */\r
+public class AntPathMatcher implements PathMatcher {\r
+\r
+       /** Default path separator: "/" */\r
+       public static final String DEFAULT_PATH_SEPARATOR = "/";\r
+\r
+       private String pathSeparator = DEFAULT_PATH_SEPARATOR;\r
+\r
+\r
+       /**\r
+        * Set the path separator to use for pattern parsing.\r
+        * Default is "/", as in Ant.\r
+        */\r
+       public void setPathSeparator(String pathSeparator) {\r
+               this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR);\r
+       }\r
+\r
+\r
+       public boolean isPattern(String path) {\r
+               return (path.indexOf('*') != -1 || path.indexOf('?') != -1);\r
+       }\r
+\r
+       public boolean match(String pattern, String path) {\r
+               return doMatch(pattern, path, true);\r
+       }\r
+\r
+       public boolean matchStart(String pattern, String path) {\r
+               return doMatch(pattern, path, false);\r
+       }\r
+\r
+\r
+       /**\r
+        * Actually match the given <code>path</code> against the given <code>pattern</code>.\r
+        * @param pattern the pattern to match against\r
+        * @param path the path String to test\r
+        * @param fullMatch whether a full pattern match is required\r
+        * (else a pattern match as far as the given base path goes is sufficient)\r
+        * @return <code>true</code> if the supplied <code>path</code> matched,\r
+        * <code>false</code> if it didn't\r
+        */\r
+       protected boolean doMatch(String pattern, String path, boolean fullMatch) {\r
+               if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) {\r
+                       return false;\r
+               }\r
+\r
+               String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);\r
+               String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator);\r
+\r
+               int pattIdxStart = 0;\r
+               int pattIdxEnd = pattDirs.length - 1;\r
+               int pathIdxStart = 0;\r
+               int pathIdxEnd = pathDirs.length - 1;\r
+\r
+               // Match all elements up to the first **\r
+               while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {\r
+                       String patDir = pattDirs[pattIdxStart];\r
+                       if ("**".equals(patDir)) {\r
+                               break;\r
+                       }\r
+                       if (!matchStrings(patDir, pathDirs[pathIdxStart])) {\r
+                               return false;\r
+                       }\r
+                       pattIdxStart++;\r
+                       pathIdxStart++;\r
+               }\r
+\r
+               if (pathIdxStart > pathIdxEnd) {\r
+                       // Path is exhausted, only match if rest of pattern is * or **'s\r
+                       if (pattIdxStart > pattIdxEnd) {\r
+                               return (pattern.endsWith(this.pathSeparator) ?\r
+                                               path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator));\r
+                       }\r
+                       if (!fullMatch) {\r
+                               return true;\r
+                       }\r
+                       if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") &&\r
+                                       path.endsWith(this.pathSeparator)) {\r
+                               return true;\r
+                       }\r
+                       for (int i = pattIdxStart; i <= pattIdxEnd; i++) {\r
+                               if (!pattDirs[i].equals("**")) {\r
+                                       return false;\r
+                               }\r
+                       }\r
+                       return true;\r
+               }\r
+               else if (pattIdxStart > pattIdxEnd) {\r
+                       // String not exhausted, but pattern is. Failure.\r
+                       return false;\r
+               }\r
+               else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) {\r
+                       // Path start definitely matches due to "**" part in pattern.\r
+                       return true;\r
+               }\r
+\r
+               // up to last '**'\r
+               while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) {\r
+                       String patDir = pattDirs[pattIdxEnd];\r
+                       if (patDir.equals("**")) {\r
+                               break;\r
+                       }\r
+                       if (!matchStrings(patDir, pathDirs[pathIdxEnd])) {\r
+                               return false;\r
+                       }\r
+                       pattIdxEnd--;\r
+                       pathIdxEnd--;\r
+               }\r
+               if (pathIdxStart > pathIdxEnd) {\r
+                       // String is exhausted\r
+                       for (int i = pattIdxStart; i <= pattIdxEnd; i++) {\r
+                               if (!pattDirs[i].equals("**")) {\r
+                                       return false;\r
+                               }\r
+                       }\r
+                       return true;\r
+               }\r
+\r
+               while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) {\r
+                       int patIdxTmp = -1;\r
+                       for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) {\r
+                               if (pattDirs[i].equals("**")) {\r
+                                       patIdxTmp = i;\r
+                                       break;\r
+                               }\r
+                       }\r
+                       if (patIdxTmp == pattIdxStart + 1) {\r
+                               // '**/**' situation, so skip one\r
+                               pattIdxStart++;\r
+                               continue;\r
+                       }\r
+                       // Find the pattern between padIdxStart & padIdxTmp in str between\r
+                       // strIdxStart & strIdxEnd\r
+                       int patLength = (patIdxTmp - pattIdxStart - 1);\r
+                       int strLength = (pathIdxEnd - pathIdxStart + 1);\r
+                       int foundIdx = -1;\r
+\r
+                       strLoop:\r
+                           for (int i = 0; i <= strLength - patLength; i++) {\r
+                                   for (int j = 0; j < patLength; j++) {\r
+                                           String subPat = (String) pattDirs[pattIdxStart + j + 1];\r
+                                           String subStr = (String) pathDirs[pathIdxStart + i + j];\r
+                                           if (!matchStrings(subPat, subStr)) {\r
+                                                   continue strLoop;\r
+                                           }\r
+                                   }\r
+                                   foundIdx = pathIdxStart + i;\r
+                                   break;\r
+                           }\r
+\r
+                       if (foundIdx == -1) {\r
+                               return false;\r
+                       }\r
+\r
+                       pattIdxStart = patIdxTmp;\r
+                       pathIdxStart = foundIdx + patLength;\r
+               }\r
+\r
+               for (int i = pattIdxStart; i <= pattIdxEnd; i++) {\r
+                       if (!pattDirs[i].equals("**")) {\r
+                               return false;\r
+                       }\r
+               }\r
+\r
+               return true;\r
+       }\r
+\r
+       /**\r
+        * Tests whether or not a string matches against a pattern.\r
+        * The pattern may contain two special characters:<br>\r
+        * '*' means zero or more characters<br>\r
+        * '?' means one and only one character\r
+        * @param pattern pattern to match against.\r
+        * Must not be <code>null</code>.\r
+        * @param str string which must be matched against the pattern.\r
+        * Must not be <code>null</code>.\r
+        * @return <code>true</code> if the string matches against the\r
+        * pattern, or <code>false</code> otherwise.\r
+        */\r
+       private boolean matchStrings(String pattern, String str) {\r
+               char[] patArr = pattern.toCharArray();\r
+               char[] strArr = str.toCharArray();\r
+               int patIdxStart = 0;\r
+               int patIdxEnd = patArr.length - 1;\r
+               int strIdxStart = 0;\r
+               int strIdxEnd = strArr.length - 1;\r
+               char ch;\r
+\r
+               boolean containsStar = false;\r
+               for (int i = 0; i < patArr.length; i++) {\r
+                       if (patArr[i] == '*') {\r
+                               containsStar = true;\r
+                               break;\r
+                       }\r
+               }\r
+\r
+               if (!containsStar) {\r
+                       // No '*'s, so we make a shortcut\r
+                       if (patIdxEnd != strIdxEnd) {\r
+                               return false; // Pattern and string do not have the same size\r
+                       }\r
+                       for (int i = 0; i <= patIdxEnd; i++) {\r
+                               ch = patArr[i];\r
+                               if (ch != '?') {\r
+                                       if (ch != strArr[i]) {\r
+                                               return false;// Character mismatch\r
+                                       }\r
+                               }\r
+                       }\r
+                       return true; // String matches against pattern\r
+               }\r
+\r
+\r
+               if (patIdxEnd == 0) {\r
+                       return true; // Pattern contains only '*', which matches anything\r
+               }\r
+\r
+               // Process characters before first star\r
+               while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) {\r
+                       if (ch != '?') {\r
+                               if (ch != strArr[strIdxStart]) {\r
+                                       return false;// Character mismatch\r
+                               }\r
+                       }\r
+                       patIdxStart++;\r
+                       strIdxStart++;\r
+               }\r
+               if (strIdxStart > strIdxEnd) {\r
+                       // All characters in the string are used. Check if only '*'s are\r
+                       // left in the pattern. If so, we succeeded. Otherwise failure.\r
+                       for (int i = patIdxStart; i <= patIdxEnd; i++) {\r
+                               if (patArr[i] != '*') {\r
+                                       return false;\r
+                               }\r
+                       }\r
+                       return true;\r
+               }\r
+\r
+               // Process characters after last star\r
+               while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) {\r
+                       if (ch != '?') {\r
+                               if (ch != strArr[strIdxEnd]) {\r
+                                       return false;// Character mismatch\r
+                               }\r
+                       }\r
+                       patIdxEnd--;\r
+                       strIdxEnd--;\r
+               }\r
+               if (strIdxStart > strIdxEnd) {\r
+                       // All characters in the string are used. Check if only '*'s are\r
+                       // left in the pattern. If so, we succeeded. Otherwise failure.\r
+                       for (int i = patIdxStart; i <= patIdxEnd; i++) {\r
+                               if (patArr[i] != '*') {\r
+                                       return false;\r
+                               }\r
+                       }\r
+                       return true;\r
+               }\r
+\r
+               // process pattern between stars. padIdxStart and patIdxEnd point\r
+               // always to a '*'.\r
+               while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {\r
+                       int patIdxTmp = -1;\r
+                       for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {\r
+                               if (patArr[i] == '*') {\r
+                                       patIdxTmp = i;\r
+                                       break;\r
+                               }\r
+                       }\r
+                       if (patIdxTmp == patIdxStart + 1) {\r
+                               // Two stars next to each other, skip the first one.\r
+                               patIdxStart++;\r
+                               continue;\r
+                       }\r
+                       // Find the pattern between padIdxStart & padIdxTmp in str between\r
+                       // strIdxStart & strIdxEnd\r
+                       int patLength = (patIdxTmp - patIdxStart - 1);\r
+                       int strLength = (strIdxEnd - strIdxStart + 1);\r
+                       int foundIdx = -1;\r
+                       strLoop:\r
+                       for (int i = 0; i <= strLength - patLength; i++) {\r
+                               for (int j = 0; j < patLength; j++) {\r
+                                       ch = patArr[patIdxStart + j + 1];\r
+                                       if (ch != '?') {\r
+                                               if (ch != strArr[strIdxStart + i + j]) {\r
+                                                       continue strLoop;\r
+                                               }\r
+                                       }\r
+                               }\r
+\r
+                               foundIdx = strIdxStart + i;\r
+                               break;\r
+                       }\r
+\r
+                       if (foundIdx == -1) {\r
+                               return false;\r
+                       }\r
+\r
+                       patIdxStart = patIdxTmp;\r
+                       strIdxStart = foundIdx + patLength;\r
+               }\r
+\r
+               // All characters in the string are used. Check if only '*'s are left\r
+               // in the pattern. If so, we succeeded. Otherwise failure.\r
+               for (int i = patIdxStart; i <= patIdxEnd; i++) {\r
+                       if (patArr[i] != '*') {\r
+                               return false;\r
+                       }\r
+               }\r
+\r
+               return true;\r
+       }\r
+\r
+       /**\r
+        * Given a pattern and a full path, determine the pattern-mapped part.\r
+        * <p>For example:\r
+        * <ul>\r
+        * <li>'<code>/docs/cvs/commit.html</code>' and '<code>/docs/cvs/commit.html</code> to ''</li>\r
+        * <li>'<code>/docs/*</code>' and '<code>/docs/cvs/commit</code> to '<code>cvs/commit</code>'</li>\r
+        * <li>'<code>/docs/cvs/*.html</code>' and '<code>/docs/cvs/commit.html</code> to '<code>commit.html</code>'</li>\r
+        * <li>'<code>/docs/**</code>' and '<code>/docs/cvs/commit</code> to '<code>cvs/commit</code>'</li>\r
+        * <li>'<code>/docs/**\/*.html</code>' and '<code>/docs/cvs/commit.html</code> to '<code>cvs/commit.html</code>'</li>\r
+        * <li>'<code>/*.html</code>' and '<code>/docs/cvs/commit.html</code> to '<code>docs/cvs/commit.html</code>'</li>\r
+        * <li>'<code>*.html</code>' and '<code>/docs/cvs/commit.html</code> to '<code>/docs/cvs/commit.html</code>'</li>\r
+        * <li>'<code>*</code>' and '<code>/docs/cvs/commit.html</code> to '<code>/docs/cvs/commit.html</code>'</li>\r
+        * </ul>\r
+        * <p>Assumes that {@link #match} returns <code>true</code> for '<code>pattern</code>'\r
+        * and '<code>path</code>', but does <strong>not</strong> enforce this.\r
+        */\r
+       public String extractPathWithinPattern(String pattern, String path) {\r
+               String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator);\r
+               String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator);\r
+\r
+               StringBuffer buffer = new StringBuffer();\r
+\r
+               // Add any path parts that have a wildcarded pattern part.\r
+               int puts = 0;\r
+               for (int i = 0; i < patternParts.length; i++) {\r
+                       String patternPart = patternParts[i];\r
+                       if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) {\r
+                               if (puts > 0 || (i == 0 && !pattern.startsWith(this.pathSeparator))) {\r
+                                       buffer.append(this.pathSeparator);\r
+                               }\r
+                               buffer.append(pathParts[i]);\r
+                               puts++;\r
+                       }\r
+               }\r
+\r
+               // Append any trailing path parts.\r
+               for (int i = patternParts.length; i < pathParts.length; i++) {\r
+                       if (puts > 0 || i > 0) {\r
+                               buffer.append(this.pathSeparator);\r
+                       }\r
+                       buffer.append(pathParts[i]);\r
+               }\r
+\r
+               return buffer.toString();\r
+       }\r
+\r
+}\r
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/CollectionUtils.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/CollectionUtils.java
new file mode 100644 (file)
index 0000000..18cbe16
--- /dev/null
@@ -0,0 +1,276 @@
+/*\r
+ * Copyright 2002-2008 the original author or authors.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.argeo.osgi.boot.internal.springutil;\r
+\r
+import java.util.Arrays;\r
+import java.util.Collection;\r
+import java.util.Enumeration;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+import java.util.Map;\r
+import java.util.Properties;\r
+\r
+/**\r
+ * Miscellaneous collection utility methods.\r
+ * Mainly for internal use within the framework.\r
+ *\r
+ * @author Juergen Hoeller\r
+ * @author Rob Harrop\r
+ * @since 1.1.3\r
+ */\r
+@SuppressWarnings({ "rawtypes", "unchecked" })\r
+public abstract class CollectionUtils {\r
+\r
+       /**\r
+        * Return <code>true</code> if the supplied Collection is <code>null</code>\r
+        * or empty. Otherwise, return <code>false</code>.\r
+        * @param collection the Collection to check\r
+        * @return whether the given Collection is empty\r
+        */\r
+       public static boolean isEmpty(Collection collection) {\r
+               return (collection == null || collection.isEmpty());\r
+       }\r
+\r
+       /**\r
+        * Return <code>true</code> if the supplied Map is <code>null</code>\r
+        * or empty. Otherwise, return <code>false</code>.\r
+        * @param map the Map to check\r
+        * @return whether the given Map is empty\r
+        */\r
+       public static boolean isEmpty(Map map) {\r
+               return (map == null || map.isEmpty());\r
+       }\r
+\r
+       /**\r
+        * Convert the supplied array into a List. A primitive array gets\r
+        * converted into a List of the appropriate wrapper type.\r
+        * <p>A <code>null</code> source value will be converted to an\r
+        * empty List.\r
+        * @param source the (potentially primitive) array\r
+        * @return the converted List result\r
+        * @see ObjectUtils#toObjectArray(Object)\r
+        */\r
+       public static List arrayToList(Object source) {\r
+               return Arrays.asList(ObjectUtils.toObjectArray(source));\r
+       }\r
+\r
+       /**\r
+        * Merge the given array into the given Collection.\r
+        * @param array the array to merge (may be <code>null</code>)\r
+        * @param collection the target Collection to merge the array into\r
+        */\r
+       public static void mergeArrayIntoCollection(Object array, Collection collection) {\r
+               if (collection == null) {\r
+                       throw new IllegalArgumentException("Collection must not be null");\r
+               }\r
+               Object[] arr = ObjectUtils.toObjectArray(array);\r
+               for (int i = 0; i < arr.length; i++) {\r
+                       collection.add(arr[i]);\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Merge the given Properties instance into the given Map,\r
+        * copying all properties (key-value pairs) over.\r
+        * <p>Uses <code>Properties.propertyNames()</code> to even catch\r
+        * default properties linked into the original Properties instance.\r
+        * @param props the Properties instance to merge (may be <code>null</code>)\r
+        * @param map the target Map to merge the properties into\r
+        */\r
+       public static void mergePropertiesIntoMap(Properties props, Map map) {\r
+               if (map == null) {\r
+                       throw new IllegalArgumentException("Map must not be null");\r
+               }\r
+               if (props != null) {\r
+                       for (Enumeration en = props.propertyNames(); en.hasMoreElements();) {\r
+                               String key = (String) en.nextElement();\r
+                               map.put(key, props.getProperty(key));\r
+                       }\r
+               }\r
+       }\r
+\r
+\r
+       /**\r
+        * Check whether the given Iterator contains the given element.\r
+        * @param iterator the Iterator to check\r
+        * @param element the element to look for\r
+        * @return <code>true</code> if found, <code>false</code> else\r
+        */\r
+       public static boolean contains(Iterator iterator, Object element) {\r
+               if (iterator != null) {\r
+                       while (iterator.hasNext()) {\r
+                               Object candidate = iterator.next();\r
+                               if (ObjectUtils.nullSafeEquals(candidate, element)) {\r
+                                       return true;\r
+                               }\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
+\r
+       /**\r
+        * Check whether the given Enumeration contains the given element.\r
+        * @param enumeration the Enumeration to check\r
+        * @param element the element to look for\r
+        * @return <code>true</code> if found, <code>false</code> else\r
+        */\r
+       public static boolean contains(Enumeration enumeration, Object element) {\r
+               if (enumeration != null) {\r
+                       while (enumeration.hasMoreElements()) {\r
+                               Object candidate = enumeration.nextElement();\r
+                               if (ObjectUtils.nullSafeEquals(candidate, element)) {\r
+                                       return true;\r
+                               }\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
+\r
+       /**\r
+        * Check whether the given Collection contains the given element instance.\r
+        * <p>Enforces the given instance to be present, rather than returning\r
+        * <code>true</code> for an equal element as well.\r
+        * @param collection the Collection to check\r
+        * @param element the element to look for\r
+        * @return <code>true</code> if found, <code>false</code> else\r
+        */\r
+       public static boolean containsInstance(Collection collection, Object element) {\r
+               if (collection != null) {\r
+                       for (Iterator it = collection.iterator(); it.hasNext();) {\r
+                               Object candidate = it.next();\r
+                               if (candidate == element) {\r
+                                       return true;\r
+                               }\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
+\r
+       /**\r
+        * Return <code>true</code> if any element in '<code>candidates</code>' is\r
+        * contained in '<code>source</code>'; otherwise returns <code>false</code>.\r
+        * @param source the source Collection\r
+        * @param candidates the candidates to search for\r
+        * @return whether any of the candidates has been found\r
+        */\r
+       public static boolean containsAny(Collection source, Collection candidates) {\r
+               if (isEmpty(source) || isEmpty(candidates)) {\r
+                       return false;\r
+               }\r
+               for (Iterator it = candidates.iterator(); it.hasNext();) {\r
+                       if (source.contains(it.next())) {\r
+                               return true;\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
+\r
+       /**\r
+        * Return the first element in '<code>candidates</code>' that is contained in\r
+        * '<code>source</code>'. If no element in '<code>candidates</code>' is present in\r
+        * '<code>source</code>' returns <code>null</code>. Iteration order is\r
+        * {@link Collection} implementation specific.\r
+        * @param source the source Collection\r
+        * @param candidates the candidates to search for\r
+        * @return the first present object, or <code>null</code> if not found\r
+        */\r
+       public static Object findFirstMatch(Collection source, Collection candidates) {\r
+               if (isEmpty(source) || isEmpty(candidates)) {\r
+                       return null;\r
+               }\r
+               for (Iterator it = candidates.iterator(); it.hasNext();) {\r
+                       Object candidate = it.next();\r
+                       if (source.contains(candidate)) {\r
+                               return candidate;\r
+                       }\r
+               }\r
+               return null;\r
+       }\r
+\r
+       /**\r
+        * Find a single value of the given type in the given Collection.\r
+        * @param collection the Collection to search\r
+        * @param type the type to look for\r
+        * @return a value of the given type found if there is a clear match,\r
+        * or <code>null</code> if none or more than one such value found\r
+        */\r
+       public static Object findValueOfType(Collection collection, Class type) {\r
+               if (isEmpty(collection)) {\r
+                       return null;\r
+               }\r
+               Object value = null;\r
+               for (Iterator it = collection.iterator(); it.hasNext();) {\r
+                       Object obj = it.next();\r
+                       if (type == null || type.isInstance(obj)) {\r
+                               if (value != null) {\r
+                                       // More than one value found... no clear single value.\r
+                                       return null;\r
+                               }\r
+                               value = obj;\r
+                       }\r
+               }\r
+               return value;\r
+       }\r
+\r
+       /**\r
+        * Find a single value of one of the given types in the given Collection:\r
+        * searching the Collection for a value of the first type, then\r
+        * searching for a value of the second type, etc.\r
+        * @param collection the collection to search\r
+        * @param types the types to look for, in prioritized order\r
+        * @return a value of one of the given types found if there is a clear match,\r
+        * or <code>null</code> if none or more than one such value found\r
+        */\r
+       public static Object findValueOfType(Collection collection, Class[] types) {\r
+               if (isEmpty(collection) || ObjectUtils.isEmpty(types)) {\r
+                       return null;\r
+               }\r
+               for (int i = 0; i < types.length; i++) {\r
+                       Object value = findValueOfType(collection, types[i]);\r
+                       if (value != null) {\r
+                               return value;\r
+                       }\r
+               }\r
+               return null;\r
+       }\r
+\r
+       /**\r
+        * Determine whether the given Collection only contains a single unique object.\r
+        * @param collection the Collection to check\r
+        * @return <code>true</code> if the collection contains a single reference or\r
+        * multiple references to the same instance, <code>false</code> else\r
+        */\r
+       public static boolean hasUniqueObject(Collection collection) {\r
+               if (isEmpty(collection)) {\r
+                       return false;\r
+               }\r
+               boolean hasCandidate = false;\r
+               Object candidate = null;\r
+               for (Iterator it = collection.iterator(); it.hasNext();) {\r
+                       Object elem = it.next();\r
+                       if (!hasCandidate) {\r
+                               hasCandidate = true;\r
+                               candidate = elem;\r
+                       }\r
+                       else if (candidate != elem) {\r
+                               return false;\r
+                       }\r
+               }\r
+               return true;\r
+       }\r
+\r
+}\r
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/ObjectUtils.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/ObjectUtils.java
new file mode 100644 (file)
index 0000000..2c98b46
--- /dev/null
@@ -0,0 +1,833 @@
+/*\r
+ * Copyright 2002-2007 the original author or authors.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.argeo.osgi.boot.internal.springutil;\r
+\r
+import java.lang.reflect.Array;\r
+import java.util.Arrays;\r
+\r
+/**\r
+ * Miscellaneous object utility methods. Mainly for internal use within the\r
+ * framework; consider Jakarta's Commons Lang for a more comprehensive suite\r
+ * of object utilities.\r
+ *\r
+ * @author Juergen Hoeller\r
+ * @author Keith Donald\r
+ * @author Rod Johnson\r
+ * @author Rob Harrop\r
+ * @author Alex Ruiz\r
+ * @since 19.03.2004\r
+ */\r
+@SuppressWarnings({ "rawtypes", "unchecked" })\r
+public abstract class ObjectUtils {\r
+\r
+       private static final int INITIAL_HASH = 7;\r
+       private static final int MULTIPLIER = 31;\r
+\r
+       private static final String EMPTY_STRING = "";\r
+       private static final String NULL_STRING = "null";\r
+       private static final String ARRAY_START = "{";\r
+       private static final String ARRAY_END = "}";\r
+       private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END;\r
+       private static final String ARRAY_ELEMENT_SEPARATOR = ", ";\r
+\r
+\r
+       /**\r
+        * Return whether the given throwable is a checked exception:\r
+        * that is, neither a RuntimeException nor an Error.\r
+        * @param ex the throwable to check\r
+        * @return whether the throwable is a checked exception\r
+        * @see java.lang.Exception\r
+        * @see java.lang.RuntimeException\r
+        * @see java.lang.Error\r
+        */\r
+       public static boolean isCheckedException(Throwable ex) {\r
+               return !(ex instanceof RuntimeException || ex instanceof Error);\r
+       }\r
+\r
+       /**\r
+        * Check whether the given exception is compatible with the exceptions\r
+        * declared in a throws clause.\r
+        * @param ex the exception to checked\r
+        * @param declaredExceptions the exceptions declared in the throws clause\r
+        * @return whether the given exception is compatible\r
+        */\r
+       public static boolean isCompatibleWithThrowsClause(Throwable ex, Class[] declaredExceptions) {\r
+               if (!isCheckedException(ex)) {\r
+                       return true;\r
+               }\r
+               if (declaredExceptions != null) {\r
+                       for (int i = 0; i < declaredExceptions.length; i++) {\r
+                               if (declaredExceptions[i].isAssignableFrom(ex.getClass())) {\r
+                                       return true;\r
+                               }\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
+\r
+       /**\r
+        * Return whether the given array is empty: that is, <code>null</code>\r
+        * or of zero length.\r
+        * @param array the array to check\r
+        * @return whether the given array is empty\r
+        */\r
+       public static boolean isEmpty(Object[] array) {\r
+               return (array == null || array.length == 0);\r
+       }\r
+\r
+       /**\r
+        * Check whether the given array contains the given element.\r
+        * @param array the array to check (may be <code>null</code>,\r
+        * in which case the return value will always be <code>false</code>)\r
+        * @param element the element to check for\r
+        * @return whether the element has been found in the given array\r
+        */\r
+       public static boolean containsElement(Object[] array, Object element) {\r
+               if (array == null) {\r
+                       return false;\r
+               }\r
+               for (int i = 0; i < array.length; i++) {\r
+                       if (nullSafeEquals(array[i], element)) {\r
+                               return true;\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
+\r
+       /**\r
+        * Append the given Object to the given array, returning a new array\r
+        * consisting of the input array contents plus the given Object.\r
+        * @param array the array to append to (can be <code>null</code>)\r
+        * @param obj the Object to append\r
+        * @return the new array (of the same component type; never <code>null</code>)\r
+        */\r
+       public static Object[] addObjectToArray(Object[] array, Object obj) {\r
+               Class compType = Object.class;\r
+               if (array != null) {\r
+                       compType = array.getClass().getComponentType();\r
+               }\r
+               else if (obj != null) {\r
+                       compType = obj.getClass();\r
+               }\r
+               int newArrLength = (array != null ? array.length + 1 : 1);\r
+               Object[] newArr = (Object[]) Array.newInstance(compType, newArrLength);\r
+               if (array != null) {\r
+                       System.arraycopy(array, 0, newArr, 0, array.length);\r
+               }\r
+               newArr[newArr.length - 1] = obj;\r
+               return newArr;\r
+       }\r
+\r
+       /**\r
+        * Convert the given array (which may be a primitive array) to an\r
+        * object array (if necessary of primitive wrapper objects).\r
+        * <p>A <code>null</code> source value will be converted to an\r
+        * empty Object array.\r
+        * @param source the (potentially primitive) array\r
+        * @return the corresponding object array (never <code>null</code>)\r
+        * @throws IllegalArgumentException if the parameter is not an array\r
+        */\r
+       public static Object[] toObjectArray(Object source) {\r
+               if (source instanceof Object[]) {\r
+                       return (Object[]) source;\r
+               }\r
+               if (source == null) {\r
+                       return new Object[0];\r
+               }\r
+               if (!source.getClass().isArray()) {\r
+                       throw new IllegalArgumentException("Source is not an array: " + source);\r
+               }\r
+               int length = Array.getLength(source);\r
+               if (length == 0) {\r
+                       return new Object[0];\r
+               }\r
+               Class wrapperType = Array.get(source, 0).getClass();\r
+               Object[] newArray = (Object[]) Array.newInstance(wrapperType, length);\r
+               for (int i = 0; i < length; i++) {\r
+                       newArray[i] = Array.get(source, i);\r
+               }\r
+               return newArray;\r
+       }\r
+\r
+\r
+       //---------------------------------------------------------------------\r
+       // Convenience methods for content-based equality/hash-code handling\r
+       //---------------------------------------------------------------------\r
+\r
+       /**\r
+        * Determine if the given objects are equal, returning <code>true</code>\r
+        * if both are <code>null</code> or <code>false</code> if only one is\r
+        * <code>null</code>.\r
+        * <p>Compares arrays with <code>Arrays.equals</code>, performing an equality\r
+        * check based on the array elements rather than the array reference.\r
+        * @param o1 first Object to compare\r
+        * @param o2 second Object to compare\r
+        * @return whether the given objects are equal\r
+        * @see java.util.Arrays#equals\r
+        */\r
+       public static boolean nullSafeEquals(Object o1, Object o2) {\r
+               if (o1 == o2) {\r
+                       return true;\r
+               }\r
+               if (o1 == null || o2 == null) {\r
+                       return false;\r
+               }\r
+               if (o1.equals(o2)) {\r
+                       return true;\r
+               }\r
+               if (o1.getClass().isArray() && o2.getClass().isArray()) {\r
+                       if (o1 instanceof Object[] && o2 instanceof Object[]) {\r
+                               return Arrays.equals((Object[]) o1, (Object[]) o2);\r
+                       }\r
+                       if (o1 instanceof boolean[] && o2 instanceof boolean[]) {\r
+                               return Arrays.equals((boolean[]) o1, (boolean[]) o2);\r
+                       }\r
+                       if (o1 instanceof byte[] && o2 instanceof byte[]) {\r
+                               return Arrays.equals((byte[]) o1, (byte[]) o2);\r
+                       }\r
+                       if (o1 instanceof char[] && o2 instanceof char[]) {\r
+                               return Arrays.equals((char[]) o1, (char[]) o2);\r
+                       }\r
+                       if (o1 instanceof double[] && o2 instanceof double[]) {\r
+                               return Arrays.equals((double[]) o1, (double[]) o2);\r
+                       }\r
+                       if (o1 instanceof float[] && o2 instanceof float[]) {\r
+                               return Arrays.equals((float[]) o1, (float[]) o2);\r
+                       }\r
+                       if (o1 instanceof int[] && o2 instanceof int[]) {\r
+                               return Arrays.equals((int[]) o1, (int[]) o2);\r
+                       }\r
+                       if (o1 instanceof long[] && o2 instanceof long[]) {\r
+                               return Arrays.equals((long[]) o1, (long[]) o2);\r
+                       }\r
+                       if (o1 instanceof short[] && o2 instanceof short[]) {\r
+                               return Arrays.equals((short[]) o1, (short[]) o2);\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
+\r
+       /**\r
+        * Return as hash code for the given object; typically the value of\r
+        * <code>{@link Object#hashCode()}</code>. If the object is an array,\r
+        * this method will delegate to any of the <code>nullSafeHashCode</code>\r
+        * methods for arrays in this class. If the object is <code>null</code>,\r
+        * this method returns 0.\r
+        * @see #nullSafeHashCode(Object[])\r
+        * @see #nullSafeHashCode(boolean[])\r
+        * @see #nullSafeHashCode(byte[])\r
+        * @see #nullSafeHashCode(char[])\r
+        * @see #nullSafeHashCode(double[])\r
+        * @see #nullSafeHashCode(float[])\r
+        * @see #nullSafeHashCode(int[])\r
+        * @see #nullSafeHashCode(long[])\r
+        * @see #nullSafeHashCode(short[])\r
+        */\r
+       public static int nullSafeHashCode(Object obj) {\r
+               if (obj == null) {\r
+                       return 0;\r
+               }\r
+               if (obj.getClass().isArray()) {\r
+                       if (obj instanceof Object[]) {\r
+                               return nullSafeHashCode((Object[]) obj);\r
+                       }\r
+                       if (obj instanceof boolean[]) {\r
+                               return nullSafeHashCode((boolean[]) obj);\r
+                       }\r
+                       if (obj instanceof byte[]) {\r
+                               return nullSafeHashCode((byte[]) obj);\r
+                       }\r
+                       if (obj instanceof char[]) {\r
+                               return nullSafeHashCode((char[]) obj);\r
+                       }\r
+                       if (obj instanceof double[]) {\r
+                               return nullSafeHashCode((double[]) obj);\r
+                       }\r
+                       if (obj instanceof float[]) {\r
+                               return nullSafeHashCode((float[]) obj);\r
+                       }\r
+                       if (obj instanceof int[]) {\r
+                               return nullSafeHashCode((int[]) obj);\r
+                       }\r
+                       if (obj instanceof long[]) {\r
+                               return nullSafeHashCode((long[]) obj);\r
+                       }\r
+                       if (obj instanceof short[]) {\r
+                               return nullSafeHashCode((short[]) obj);\r
+                       }\r
+               }\r
+               return obj.hashCode();\r
+       }\r
+\r
+       /**\r
+        * Return a hash code based on the contents of the specified array.\r
+        * If <code>array</code> is <code>null</code>, this method returns 0.\r
+        */\r
+       public static int nullSafeHashCode(Object[] array) {\r
+               if (array == null) {\r
+                       return 0;\r
+               }\r
+               int hash = INITIAL_HASH;\r
+               int arraySize = array.length;\r
+               for (int i = 0; i < arraySize; i++) {\r
+                       hash = MULTIPLIER * hash + nullSafeHashCode(array[i]);\r
+               }\r
+               return hash;\r
+       }\r
+\r
+       /**\r
+        * Return a hash code based on the contents of the specified array.\r
+        * If <code>array</code> is <code>null</code>, this method returns 0.\r
+        */\r
+       public static int nullSafeHashCode(boolean[] array) {\r
+               if (array == null) {\r
+                       return 0;\r
+               }\r
+               int hash = INITIAL_HASH;\r
+               int arraySize = array.length;\r
+               for (int i = 0; i < arraySize; i++) {\r
+                       hash = MULTIPLIER * hash + hashCode(array[i]);\r
+               }\r
+               return hash;\r
+       }\r
+\r
+       /**\r
+        * Return a hash code based on the contents of the specified array.\r
+        * If <code>array</code> is <code>null</code>, this method returns 0.\r
+        */\r
+       public static int nullSafeHashCode(byte[] array) {\r
+               if (array == null) {\r
+                       return 0;\r
+               }\r
+               int hash = INITIAL_HASH;\r
+               int arraySize = array.length;\r
+               for (int i = 0; i < arraySize; i++) {\r
+                       hash = MULTIPLIER * hash + array[i];\r
+               }\r
+               return hash;\r
+       }\r
+\r
+       /**\r
+        * Return a hash code based on the contents of the specified array.\r
+        * If <code>array</code> is <code>null</code>, this method returns 0.\r
+        */\r
+       public static int nullSafeHashCode(char[] array) {\r
+               if (array == null) {\r
+                       return 0;\r
+               }\r
+               int hash = INITIAL_HASH;\r
+               int arraySize = array.length;\r
+               for (int i = 0; i < arraySize; i++) {\r
+                       hash = MULTIPLIER * hash + array[i];\r
+               }\r
+               return hash;\r
+       }\r
+\r
+       /**\r
+        * Return a hash code based on the contents of the specified array.\r
+        * If <code>array</code> is <code>null</code>, this method returns 0.\r
+        */\r
+       public static int nullSafeHashCode(double[] array) {\r
+               if (array == null) {\r
+                       return 0;\r
+               }\r
+               int hash = INITIAL_HASH;\r
+               int arraySize = array.length;\r
+               for (int i = 0; i < arraySize; i++) {\r
+                       hash = MULTIPLIER * hash + hashCode(array[i]);\r
+               }\r
+               return hash;\r
+       }\r
+\r
+       /**\r
+        * Return a hash code based on the contents of the specified array.\r
+        * If <code>array</code> is <code>null</code>, this method returns 0.\r
+        */\r
+       public static int nullSafeHashCode(float[] array) {\r
+               if (array == null) {\r
+                       return 0;\r
+               }\r
+               int hash = INITIAL_HASH;\r
+               int arraySize = array.length;\r
+               for (int i = 0; i < arraySize; i++) {\r
+                       hash = MULTIPLIER * hash + hashCode(array[i]);\r
+               }\r
+               return hash;\r
+       }\r
+\r
+       /**\r
+        * Return a hash code based on the contents of the specified array.\r
+        * If <code>array</code> is <code>null</code>, this method returns 0.\r
+        */\r
+       public static int nullSafeHashCode(int[] array) {\r
+               if (array == null) {\r
+                       return 0;\r
+               }\r
+               int hash = INITIAL_HASH;\r
+               int arraySize = array.length;\r
+               for (int i = 0; i < arraySize; i++) {\r
+                       hash = MULTIPLIER * hash + array[i];\r
+               }\r
+               return hash;\r
+       }\r
+\r
+       /**\r
+        * Return a hash code based on the contents of the specified array.\r
+        * If <code>array</code> is <code>null</code>, this method returns 0.\r
+        */\r
+       public static int nullSafeHashCode(long[] array) {\r
+               if (array == null) {\r
+                       return 0;\r
+               }\r
+               int hash = INITIAL_HASH;\r
+               int arraySize = array.length;\r
+               for (int i = 0; i < arraySize; i++) {\r
+                       hash = MULTIPLIER * hash + hashCode(array[i]);\r
+               }\r
+               return hash;\r
+       }\r
+\r
+       /**\r
+        * Return a hash code based on the contents of the specified array.\r
+        * If <code>array</code> is <code>null</code>, this method returns 0.\r
+        */\r
+       public static int nullSafeHashCode(short[] array) {\r
+               if (array == null) {\r
+                       return 0;\r
+               }\r
+               int hash = INITIAL_HASH;\r
+               int arraySize = array.length;\r
+               for (int i = 0; i < arraySize; i++) {\r
+                       hash = MULTIPLIER * hash + array[i];\r
+               }\r
+               return hash;\r
+       }\r
+\r
+       /**\r
+        * Return the same value as <code>{@link Boolean#hashCode()}</code>.\r
+        * @see Boolean#hashCode()\r
+        */\r
+       public static int hashCode(boolean bool) {\r
+               return bool ? 1231 : 1237;\r
+       }\r
+\r
+       /**\r
+        * Return the same value as <code>{@link Double#hashCode()}</code>.\r
+        * @see Double#hashCode()\r
+        */\r
+       public static int hashCode(double dbl) {\r
+               long bits = Double.doubleToLongBits(dbl);\r
+               return hashCode(bits);\r
+       }\r
+\r
+       /**\r
+        * Return the same value as <code>{@link Float#hashCode()}</code>.\r
+        * @see Float#hashCode()\r
+        */\r
+       public static int hashCode(float flt) {\r
+               return Float.floatToIntBits(flt);\r
+       }\r
+\r
+       /**\r
+        * Return the same value as <code>{@link Long#hashCode()}</code>.\r
+        * @see Long#hashCode()\r
+        */\r
+       public static int hashCode(long lng) {\r
+               return (int) (lng ^ (lng >>> 32));\r
+       }\r
+\r
+\r
+       //---------------------------------------------------------------------\r
+       // Convenience methods for toString output\r
+       //---------------------------------------------------------------------\r
+\r
+       /**\r
+        * Return a String representation of an object's overall identity.\r
+        * @param obj the object (may be <code>null</code>)\r
+        * @return the object's identity as String representation,\r
+        * or an empty String if the object was <code>null</code>\r
+        */\r
+       public static String identityToString(Object obj) {\r
+               if (obj == null) {\r
+                       return EMPTY_STRING;\r
+               }\r
+               return obj.getClass().getName() + "@" + getIdentityHexString(obj);\r
+       }\r
+\r
+       /**\r
+        * Return a hex String form of an object's identity hash code.\r
+        * @param obj the object\r
+        * @return the object's identity code in hex notation\r
+        */\r
+       public static String getIdentityHexString(Object obj) {\r
+               return Integer.toHexString(System.identityHashCode(obj));\r
+       }\r
+\r
+       /**\r
+        * Return a content-based String representation if <code>obj</code> is\r
+        * not <code>null</code>; otherwise returns an empty String.\r
+        * <p>Differs from {@link #nullSafeToString(Object)} in that it returns\r
+        * an empty String rather than "null" for a <code>null</code> value.\r
+        * @param obj the object to build a display String for\r
+        * @return a display String representation of <code>obj</code>\r
+        * @see #nullSafeToString(Object)\r
+        */\r
+       public static String getDisplayString(Object obj) {\r
+               if (obj == null) {\r
+                       return EMPTY_STRING;\r
+               }\r
+               return nullSafeToString(obj);\r
+       }\r
+\r
+       /**\r
+        * Determine the class name for the given object.\r
+        * <p>Returns <code>"null"</code> if <code>obj</code> is <code>null</code>.\r
+        * @param obj the object to introspect (may be <code>null</code>)\r
+        * @return the corresponding class name\r
+        */\r
+       public static String nullSafeClassName(Object obj) {\r
+               return (obj != null ? obj.getClass().getName() : NULL_STRING);\r
+       }\r
+\r
+       /**\r
+        * Return a String representation of the specified Object.\r
+        * <p>Builds a String representation of the contents in case of an array.\r
+        * Returns <code>"null"</code> if <code>obj</code> is <code>null</code>.\r
+        * @param obj the object to build a String representation for\r
+        * @return a String representation of <code>obj</code>\r
+        */\r
+       public static String nullSafeToString(Object obj) {\r
+               if (obj == null) {\r
+                       return NULL_STRING;\r
+               }\r
+               if (obj instanceof String) {\r
+                       return (String) obj;\r
+               }\r
+               if (obj instanceof Object[]) {\r
+                       return nullSafeToString((Object[]) obj);\r
+               }\r
+               if (obj instanceof boolean[]) {\r
+                       return nullSafeToString((boolean[]) obj);\r
+               }\r
+               if (obj instanceof byte[]) {\r
+                       return nullSafeToString((byte[]) obj);\r
+               }\r
+               if (obj instanceof char[]) {\r
+                       return nullSafeToString((char[]) obj);\r
+               }\r
+               if (obj instanceof double[]) {\r
+                       return nullSafeToString((double[]) obj);\r
+               }\r
+               if (obj instanceof float[]) {\r
+                       return nullSafeToString((float[]) obj);\r
+               }\r
+               if (obj instanceof int[]) {\r
+                       return nullSafeToString((int[]) obj);\r
+               }\r
+               if (obj instanceof long[]) {\r
+                       return nullSafeToString((long[]) obj);\r
+               }\r
+               if (obj instanceof short[]) {\r
+                       return nullSafeToString((short[]) obj);\r
+               }\r
+               String str = obj.toString();\r
+               return (str != null ? str : EMPTY_STRING);\r
+       }\r
+\r
+       /**\r
+        * Return a String representation of the contents of the specified array.\r
+        * <p>The String representation consists of a list of the array's elements,\r
+        * enclosed in curly braces (<code>"{}"</code>). Adjacent elements are separated\r
+        * by the characters <code>", "</code> (a comma followed by a space). Returns\r
+        * <code>"null"</code> if <code>array</code> is <code>null</code>.\r
+        * @param array the array to build a String representation for\r
+        * @return a String representation of <code>array</code>\r
+        */\r
+       public static String nullSafeToString(Object[] array) {\r
+               if (array == null) {\r
+                       return NULL_STRING;\r
+               }\r
+               int length = array.length;\r
+               if (length == 0) {\r
+                       return EMPTY_ARRAY;\r
+               }\r
+               StringBuffer buffer = new StringBuffer();\r
+               for (int i = 0; i < length; i++) {\r
+                       if (i == 0) {\r
+                               buffer.append(ARRAY_START);\r
+                       }\r
+                       else {\r
+                               buffer.append(ARRAY_ELEMENT_SEPARATOR);\r
+                       }\r
+                       buffer.append(String.valueOf(array[i]));\r
+               }\r
+               buffer.append(ARRAY_END);\r
+               return buffer.toString();\r
+       }\r
+\r
+       /**\r
+        * Return a String representation of the contents of the specified array.\r
+        * <p>The String representation consists of a list of the array's elements,\r
+        * enclosed in curly braces (<code>"{}"</code>). Adjacent elements are separated\r
+        * by the characters <code>", "</code> (a comma followed by a space). Returns\r
+        * <code>"null"</code> if <code>array</code> is <code>null</code>.\r
+        * @param array the array to build a String representation for\r
+        * @return a String representation of <code>array</code>\r
+        */\r
+       public static String nullSafeToString(boolean[] array) {\r
+               if (array == null) {\r
+                       return NULL_STRING;\r
+               }\r
+               int length = array.length;\r
+               if (length == 0) {\r
+                       return EMPTY_ARRAY;\r
+               }\r
+               StringBuffer buffer = new StringBuffer();\r
+               for (int i = 0; i < length; i++) {\r
+                       if (i == 0) {\r
+                               buffer.append(ARRAY_START);\r
+                       }\r
+                       else {\r
+                               buffer.append(ARRAY_ELEMENT_SEPARATOR);\r
+                       }\r
+\r
+                       buffer.append(array[i]);\r
+               }\r
+               buffer.append(ARRAY_END);\r
+               return buffer.toString();\r
+       }\r
+\r
+       /**\r
+        * Return a String representation of the contents of the specified array.\r
+        * <p>The String representation consists of a list of the array's elements,\r
+        * enclosed in curly braces (<code>"{}"</code>). Adjacent elements are separated\r
+        * by the characters <code>", "</code> (a comma followed by a space). Returns\r
+        * <code>"null"</code> if <code>array</code> is <code>null</code>.\r
+        * @param array the array to build a String representation for\r
+        * @return a String representation of <code>array</code>\r
+        */\r
+       public static String nullSafeToString(byte[] array) {\r
+               if (array == null) {\r
+                       return NULL_STRING;\r
+               }\r
+               int length = array.length;\r
+               if (length == 0) {\r
+                       return EMPTY_ARRAY;\r
+               }\r
+               StringBuffer buffer = new StringBuffer();\r
+               for (int i = 0; i < length; i++) {\r
+                       if (i == 0) {\r
+                               buffer.append(ARRAY_START);\r
+                       }\r
+                       else {\r
+                               buffer.append(ARRAY_ELEMENT_SEPARATOR);\r
+                       }\r
+                       buffer.append(array[i]);\r
+               }\r
+               buffer.append(ARRAY_END);\r
+               return buffer.toString();\r
+       }\r
+\r
+       /**\r
+        * Return a String representation of the contents of the specified array.\r
+        * <p>The String representation consists of a list of the array's elements,\r
+        * enclosed in curly braces (<code>"{}"</code>). Adjacent elements are separated\r
+        * by the characters <code>", "</code> (a comma followed by a space). Returns\r
+        * <code>"null"</code> if <code>array</code> is <code>null</code>.\r
+        * @param array the array to build a String representation for\r
+        * @return a String representation of <code>array</code>\r
+        */\r
+       public static String nullSafeToString(char[] array) {\r
+               if (array == null) {\r
+                       return NULL_STRING;\r
+               }\r
+               int length = array.length;\r
+               if (length == 0) {\r
+                       return EMPTY_ARRAY;\r
+               }\r
+               StringBuffer buffer = new StringBuffer();\r
+               for (int i = 0; i < length; i++) {\r
+                       if (i == 0) {\r
+                               buffer.append(ARRAY_START);\r
+                       }\r
+                       else {\r
+                               buffer.append(ARRAY_ELEMENT_SEPARATOR);\r
+                       }\r
+                       buffer.append("'").append(array[i]).append("'");\r
+               }\r
+               buffer.append(ARRAY_END);\r
+               return buffer.toString();\r
+       }\r
+\r
+       /**\r
+        * Return a String representation of the contents of the specified array.\r
+        * <p>The String representation consists of a list of the array's elements,\r
+        * enclosed in curly braces (<code>"{}"</code>). Adjacent elements are separated\r
+        * by the characters <code>", "</code> (a comma followed by a space). Returns\r
+        * <code>"null"</code> if <code>array</code> is <code>null</code>.\r
+        * @param array the array to build a String representation for\r
+        * @return a String representation of <code>array</code>\r
+        */\r
+       public static String nullSafeToString(double[] array) {\r
+               if (array == null) {\r
+                       return NULL_STRING;\r
+               }\r
+               int length = array.length;\r
+               if (length == 0) {\r
+                       return EMPTY_ARRAY;\r
+               }\r
+               StringBuffer buffer = new StringBuffer();\r
+               for (int i = 0; i < length; i++) {\r
+                       if (i == 0) {\r
+                               buffer.append(ARRAY_START);\r
+                       }\r
+                       else {\r
+                               buffer.append(ARRAY_ELEMENT_SEPARATOR);\r
+                       }\r
+\r
+                       buffer.append(array[i]);\r
+               }\r
+               buffer.append(ARRAY_END);\r
+               return buffer.toString();\r
+       }\r
+\r
+       /**\r
+        * Return a String representation of the contents of the specified array.\r
+        * <p>The String representation consists of a list of the array's elements,\r
+        * enclosed in curly braces (<code>"{}"</code>). Adjacent elements are separated\r
+        * by the characters <code>", "</code> (a comma followed by a space). Returns\r
+        * <code>"null"</code> if <code>array</code> is <code>null</code>.\r
+        * @param array the array to build a String representation for\r
+        * @return a String representation of <code>array</code>\r
+        */\r
+       public static String nullSafeToString(float[] array) {\r
+               if (array == null) {\r
+                       return NULL_STRING;\r
+               }\r
+               int length = array.length;\r
+               if (length == 0) {\r
+                       return EMPTY_ARRAY;\r
+               }\r
+               StringBuffer buffer = new StringBuffer();\r
+               for (int i = 0; i < length; i++) {\r
+                       if (i == 0) {\r
+                               buffer.append(ARRAY_START);\r
+                       }\r
+                       else {\r
+                               buffer.append(ARRAY_ELEMENT_SEPARATOR);\r
+                       }\r
+\r
+                       buffer.append(array[i]);\r
+               }\r
+               buffer.append(ARRAY_END);\r
+               return buffer.toString();\r
+       }\r
+\r
+       /**\r
+        * Return a String representation of the contents of the specified array.\r
+        * <p>The String representation consists of a list of the array's elements,\r
+        * enclosed in curly braces (<code>"{}"</code>). Adjacent elements are separated\r
+        * by the characters <code>", "</code> (a comma followed by a space). Returns\r
+        * <code>"null"</code> if <code>array</code> is <code>null</code>.\r
+        * @param array the array to build a String representation for\r
+        * @return a String representation of <code>array</code>\r
+        */\r
+       public static String nullSafeToString(int[] array) {\r
+               if (array == null) {\r
+                       return NULL_STRING;\r
+               }\r
+               int length = array.length;\r
+               if (length == 0) {\r
+                       return EMPTY_ARRAY;\r
+               }\r
+               StringBuffer buffer = new StringBuffer();\r
+               for (int i = 0; i < length; i++) {\r
+                       if (i == 0) {\r
+                               buffer.append(ARRAY_START);\r
+                       }\r
+                       else {\r
+                               buffer.append(ARRAY_ELEMENT_SEPARATOR);\r
+                       }\r
+                       buffer.append(array[i]);\r
+               }\r
+               buffer.append(ARRAY_END);\r
+               return buffer.toString();\r
+       }\r
+\r
+       /**\r
+        * Return a String representation of the contents of the specified array.\r
+        * <p>The String representation consists of a list of the array's elements,\r
+        * enclosed in curly braces (<code>"{}"</code>). Adjacent elements are separated\r
+        * by the characters <code>", "</code> (a comma followed by a space). Returns\r
+        * <code>"null"</code> if <code>array</code> is <code>null</code>.\r
+        * @param array the array to build a String representation for\r
+        * @return a String representation of <code>array</code>\r
+        */\r
+       public static String nullSafeToString(long[] array) {\r
+               if (array == null) {\r
+                       return NULL_STRING;\r
+               }\r
+               int length = array.length;\r
+               if (length == 0) {\r
+                       return EMPTY_ARRAY;\r
+               }\r
+               StringBuffer buffer = new StringBuffer();\r
+               for (int i = 0; i < length; i++) {\r
+                       if (i == 0) {\r
+                               buffer.append(ARRAY_START);\r
+                       }\r
+                       else {\r
+                               buffer.append(ARRAY_ELEMENT_SEPARATOR);\r
+                       }\r
+                       buffer.append(array[i]);\r
+               }\r
+               buffer.append(ARRAY_END);\r
+               return buffer.toString();\r
+       }\r
+\r
+       /**\r
+        * Return a String representation of the contents of the specified array.\r
+        * <p>The String representation consists of a list of the array's elements,\r
+        * enclosed in curly braces (<code>"{}"</code>). Adjacent elements are separated\r
+        * by the characters <code>", "</code> (a comma followed by a space). Returns\r
+        * <code>"null"</code> if <code>array</code> is <code>null</code>.\r
+        * @param array the array to build a String representation for\r
+        * @return a String representation of <code>array</code>\r
+        */\r
+       public static String nullSafeToString(short[] array) {\r
+               if (array == null) {\r
+                       return NULL_STRING;\r
+               }\r
+               int length = array.length;\r
+               if (length == 0) {\r
+                       return EMPTY_ARRAY;\r
+               }\r
+               StringBuffer buffer = new StringBuffer();\r
+               for (int i = 0; i < length; i++) {\r
+                       if (i == 0) {\r
+                               buffer.append(ARRAY_START);\r
+                       }\r
+                       else {\r
+                               buffer.append(ARRAY_ELEMENT_SEPARATOR);\r
+                       }\r
+                       buffer.append(array[i]);\r
+               }\r
+               buffer.append(ARRAY_END);\r
+               return buffer.toString();\r
+       }\r
+\r
+}\r
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/PathMatcher.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/PathMatcher.java
new file mode 100644 (file)
index 0000000..d7a2322
--- /dev/null
@@ -0,0 +1,91 @@
+/*\r
+ * Copyright 2002-2007 the original author or authors.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.argeo.osgi.boot.internal.springutil;\r
+\r
+/**\r
+ * Strategy interface for <code>String</code>-based path matching.\r
+ * \r
+ * <p>Used by {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver},\r
+ * {@link org.springframework.web.servlet.handler.AbstractUrlHandlerMapping},\r
+ * {@link org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver},\r
+ * and {@link org.springframework.web.servlet.mvc.WebContentInterceptor}.\r
+ *\r
+ * <p>The default implementation is {@link AntPathMatcher}, supporting the\r
+ * Ant-style pattern syntax.\r
+ *\r
+ * @author Juergen Hoeller\r
+ * @since 1.2\r
+ * @see AntPathMatcher\r
+ */\r
+public interface PathMatcher {\r
+\r
+       /**\r
+        * Does the given <code>path</code> represent a pattern that can be matched\r
+        * by an implementation of this interface?\r
+        * <p>If the return value is <code>false</code>, then the {@link #match}\r
+        * method does not have to be used because direct equality comparisons\r
+        * on the static path Strings will lead to the same result.\r
+        * @param path the path String to check\r
+        * @return <code>true</code> if the given <code>path</code> represents a pattern\r
+        */\r
+       boolean isPattern(String path);\r
+\r
+       /**\r
+        * Match the given <code>path</code> against the given <code>pattern</code>,\r
+        * according to this PathMatcher's matching strategy.\r
+        * @param pattern the pattern to match against\r
+        * @param path the path String to test\r
+        * @return <code>true</code> if the supplied <code>path</code> matched,\r
+        * <code>false</code> if it didn't\r
+        */\r
+       boolean match(String pattern, String path);\r
+\r
+       /**\r
+        * Match the given <code>path</code> against the corresponding part of the given\r
+        * <code>pattern</code>, according to this PathMatcher's matching strategy.\r
+        * <p>Determines whether the pattern at least matches as far as the given base\r
+        * path goes, assuming that a full path may then match as well.\r
+        * @param pattern the pattern to match against\r
+        * @param path the path String to test\r
+        * @return <code>true</code> if the supplied <code>path</code> matched,\r
+        * <code>false</code> if it didn't\r
+        */\r
+       boolean matchStart(String pattern, String path);\r
+\r
+       /**\r
+        * Given a pattern and a full path, determine the pattern-mapped part.\r
+        * <p>This method is supposed to find out which part of the path is matched\r
+        * dynamically through an actual pattern, that is, it strips off a statically\r
+        * defined leading path from the given full path, returning only the actually\r
+        * pattern-matched part of the path.\r
+        * <p>For example: For "myroot/*.html" as pattern and "myroot/myfile.html"\r
+        * as full path, this method should return "myfile.html". The detailed\r
+        * determination rules are specified to this PathMatcher's matching strategy.\r
+        * <p>A simple implementation may return the given full path as-is in case\r
+        * of an actual pattern, and the empty String in case of the pattern not\r
+        * containing any dynamic parts (i.e. the <code>pattern</code> parameter being\r
+        * a static path that wouldn't qualify as an actual {@link #isPattern pattern}).\r
+        * A sophisticated implementation will differentiate between the static parts\r
+        * and the dynamic parts of the given path pattern.\r
+        * @param pattern the path pattern\r
+        * @param path the full path to introspect\r
+        * @return the pattern-mapped part of the given <code>path</code>\r
+        * (never <code>null</code>)\r
+        */\r
+       String extractPathWithinPattern(String pattern, String path);\r
+\r
+}\r
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/StringUtils.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/StringUtils.java
new file mode 100644 (file)
index 0000000..6cbaee8
--- /dev/null
@@ -0,0 +1,1113 @@
+/*\r
+ * Copyright 2002-2008 the original author or authors.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.argeo.osgi.boot.internal.springutil;\r
+\r
+import java.util.ArrayList;\r
+import java.util.Arrays;\r
+import java.util.Collection;\r
+import java.util.Collections;\r
+import java.util.Enumeration;\r
+import java.util.Iterator;\r
+import java.util.LinkedList;\r
+import java.util.List;\r
+import java.util.Locale;\r
+import java.util.Properties;\r
+import java.util.Set;\r
+import java.util.StringTokenizer;\r
+import java.util.TreeSet;\r
+\r
+/**\r
+ * Miscellaneous {@link String} utility methods.\r
+ *\r
+ * <p>Mainly for internal use within the framework; consider\r
+ * <a href="http://jakarta.apache.org/commons/lang/">Jakarta's Commons Lang</a>\r
+ * for a more comprehensive suite of String utilities.\r
+ *\r
+ * <p>This class delivers some simple functionality that should really\r
+ * be provided by the core Java <code>String</code> and {@link StringBuffer}\r
+ * classes, such as the ability to {@link #replace} all occurrences of a given\r
+ * substring in a target string. It also provides easy-to-use methods to convert\r
+ * between delimited strings, such as CSV strings, and collections and arrays.\r
+ *\r
+ * @author Rod Johnson\r
+ * @author Juergen Hoeller\r
+ * @author Keith Donald\r
+ * @author Rob Harrop\r
+ * @author Rick Evans\r
+ * @since 16 April 2001\r
+ */\r
+@SuppressWarnings({ "rawtypes", "unchecked" })\r
+public abstract class StringUtils {\r
+\r
+       private static final String FOLDER_SEPARATOR = "/";\r
+\r
+       private static final String WINDOWS_FOLDER_SEPARATOR = "\\";\r
+\r
+       private static final String TOP_PATH = "..";\r
+\r
+       private static final String CURRENT_PATH = ".";\r
+\r
+       private static final char EXTENSION_SEPARATOR = '.';\r
+\r
+\r
+       //---------------------------------------------------------------------\r
+       // General convenience methods for working with Strings\r
+       //---------------------------------------------------------------------\r
+\r
+       /**\r
+        * Check that the given CharSequence is neither <code>null</code> nor of length 0.\r
+        * Note: Will return <code>true</code> for a CharSequence that purely consists of whitespace.\r
+        * <p><pre>\r
+        * StringUtils.hasLength(null) = false\r
+        * StringUtils.hasLength("") = false\r
+        * StringUtils.hasLength(" ") = true\r
+        * StringUtils.hasLength("Hello") = true\r
+        * </pre>\r
+        * @param str the CharSequence to check (may be <code>null</code>)\r
+        * @return <code>true</code> if the CharSequence is not null and has length\r
+        * @see #hasText(String)\r
+        */\r
+       public static boolean hasLength(CharSequence str) {\r
+               return (str != null && str.length() > 0);\r
+       }\r
+\r
+       /**\r
+        * Check that the given String is neither <code>null</code> nor of length 0.\r
+        * Note: Will return <code>true</code> for a String that purely consists of whitespace.\r
+        * @param str the String to check (may be <code>null</code>)\r
+        * @return <code>true</code> if the String is not null and has length\r
+        * @see #hasLength(CharSequence)\r
+        */\r
+       public static boolean hasLength(String str) {\r
+               return hasLength((CharSequence) str);\r
+       }\r
+\r
+       /**\r
+        * Check whether the given CharSequence has actual text.\r
+        * More specifically, returns <code>true</code> if the string not <code>null</code>,\r
+        * its length is greater than 0, and it contains at least one non-whitespace character.\r
+        * <p><pre>\r
+        * StringUtils.hasText(null) = false\r
+        * StringUtils.hasText("") = false\r
+        * StringUtils.hasText(" ") = false\r
+        * StringUtils.hasText("12345") = true\r
+        * StringUtils.hasText(" 12345 ") = true\r
+        * </pre>\r
+        * @param str the CharSequence to check (may be <code>null</code>)\r
+        * @return <code>true</code> if the CharSequence is not <code>null</code>,\r
+        * its length is greater than 0, and it does not contain whitespace only\r
+        * @see java.lang.Character#isWhitespace\r
+        */\r
+       public static boolean hasText(CharSequence str) {\r
+               if (!hasLength(str)) {\r
+                       return false;\r
+               }\r
+               int strLen = str.length();\r
+               for (int i = 0; i < strLen; i++) {\r
+                       if (!Character.isWhitespace(str.charAt(i))) {\r
+                               return true;\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
+\r
+       /**\r
+        * Check whether the given String has actual text.\r
+        * More specifically, returns <code>true</code> if the string not <code>null</code>,\r
+        * its length is greater than 0, and it contains at least one non-whitespace character.\r
+        * @param str the String to check (may be <code>null</code>)\r
+        * @return <code>true</code> if the String is not <code>null</code>, its length is\r
+        * greater than 0, and it does not contain whitespace only\r
+        * @see #hasText(CharSequence)\r
+        */\r
+       public static boolean hasText(String str) {\r
+               return hasText((CharSequence) str);\r
+       }\r
+\r
+       /**\r
+        * Check whether the given CharSequence contains any whitespace characters.\r
+        * @param str the CharSequence to check (may be <code>null</code>)\r
+        * @return <code>true</code> if the CharSequence is not empty and\r
+        * contains at least 1 whitespace character\r
+        * @see java.lang.Character#isWhitespace\r
+        */\r
+       public static boolean containsWhitespace(CharSequence str) {\r
+               if (!hasLength(str)) {\r
+                       return false;\r
+               }\r
+               int strLen = str.length();\r
+               for (int i = 0; i < strLen; i++) {\r
+                       if (Character.isWhitespace(str.charAt(i))) {\r
+                               return true;\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
+\r
+       /**\r
+        * Check whether the given String contains any whitespace characters.\r
+        * @param str the String to check (may be <code>null</code>)\r
+        * @return <code>true</code> if the String is not empty and\r
+        * contains at least 1 whitespace character\r
+        * @see #containsWhitespace(CharSequence)\r
+        */\r
+       public static boolean containsWhitespace(String str) {\r
+               return containsWhitespace((CharSequence) str);\r
+       }\r
+\r
+       /**\r
+        * Trim leading and trailing whitespace from the given String.\r
+        * @param str the String to check\r
+        * @return the trimmed String\r
+        * @see java.lang.Character#isWhitespace\r
+        */\r
+       public static String trimWhitespace(String str) {\r
+               if (!hasLength(str)) {\r
+                       return str;\r
+               }\r
+               StringBuffer buf = new StringBuffer(str);\r
+               while (buf.length() > 0 && Character.isWhitespace(buf.charAt(0))) {\r
+                       buf.deleteCharAt(0);\r
+               }\r
+               while (buf.length() > 0 && Character.isWhitespace(buf.charAt(buf.length() - 1))) {\r
+                       buf.deleteCharAt(buf.length() - 1);\r
+               }\r
+               return buf.toString();\r
+       }\r
+\r
+       /**\r
+        * Trim <i>all</i> whitespace from the given String:\r
+        * leading, trailing, and inbetween characters.\r
+        * @param str the String to check\r
+        * @return the trimmed String\r
+        * @see java.lang.Character#isWhitespace\r
+        */\r
+       public static String trimAllWhitespace(String str) {\r
+               if (!hasLength(str)) {\r
+                       return str;\r
+               }\r
+               StringBuffer buf = new StringBuffer(str);\r
+               int index = 0;\r
+               while (buf.length() > index) {\r
+                       if (Character.isWhitespace(buf.charAt(index))) {\r
+                               buf.deleteCharAt(index);\r
+                       }\r
+                       else {\r
+                               index++;\r
+                       }\r
+               }\r
+               return buf.toString();\r
+       }\r
+\r
+       /**\r
+        * Trim leading whitespace from the given String.\r
+        * @param str the String to check\r
+        * @return the trimmed String\r
+        * @see java.lang.Character#isWhitespace\r
+        */\r
+       public static String trimLeadingWhitespace(String str) {\r
+               if (!hasLength(str)) {\r
+                       return str;\r
+               }\r
+               StringBuffer buf = new StringBuffer(str);\r
+               while (buf.length() > 0 && Character.isWhitespace(buf.charAt(0))) {\r
+                       buf.deleteCharAt(0);\r
+               }\r
+               return buf.toString();\r
+       }\r
+\r
+       /**\r
+        * Trim trailing whitespace from the given String.\r
+        * @param str the String to check\r
+        * @return the trimmed String\r
+        * @see java.lang.Character#isWhitespace\r
+        */\r
+       public static String trimTrailingWhitespace(String str) {\r
+               if (!hasLength(str)) {\r
+                       return str;\r
+               }\r
+               StringBuffer buf = new StringBuffer(str);\r
+               while (buf.length() > 0 && Character.isWhitespace(buf.charAt(buf.length() - 1))) {\r
+                       buf.deleteCharAt(buf.length() - 1);\r
+               }\r
+               return buf.toString();\r
+       }\r
+\r
+       /**\r
+        * Trim all occurences of the supplied leading character from the given String.\r
+        * @param str the String to check\r
+        * @param leadingCharacter the leading character to be trimmed\r
+        * @return the trimmed String\r
+        */\r
+       public static String trimLeadingCharacter(String str, char leadingCharacter) {\r
+               if (!hasLength(str)) {\r
+                       return str;\r
+               }\r
+               StringBuffer buf = new StringBuffer(str);\r
+               while (buf.length() > 0 && buf.charAt(0) == leadingCharacter) {\r
+                       buf.deleteCharAt(0);\r
+               }\r
+               return buf.toString();\r
+       }\r
+\r
+       /**\r
+        * Trim all occurences of the supplied trailing character from the given String.\r
+        * @param str the String to check\r
+        * @param trailingCharacter the trailing character to be trimmed\r
+        * @return the trimmed String\r
+        */\r
+       public static String trimTrailingCharacter(String str, char trailingCharacter) {\r
+               if (!hasLength(str)) {\r
+                       return str;\r
+               }\r
+               StringBuffer buf = new StringBuffer(str);\r
+               while (buf.length() > 0 && buf.charAt(buf.length() - 1) == trailingCharacter) {\r
+                       buf.deleteCharAt(buf.length() - 1);\r
+               }\r
+               return buf.toString();\r
+       }\r
+\r
+\r
+       /**\r
+        * Test if the given String starts with the specified prefix,\r
+        * ignoring upper/lower case.\r
+        * @param str the String to check\r
+        * @param prefix the prefix to look for\r
+        * @see java.lang.String#startsWith\r
+        */\r
+       public static boolean startsWithIgnoreCase(String str, String prefix) {\r
+               if (str == null || prefix == null) {\r
+                       return false;\r
+               }\r
+               if (str.startsWith(prefix)) {\r
+                       return true;\r
+               }\r
+               if (str.length() < prefix.length()) {\r
+                       return false;\r
+               }\r
+               String lcStr = str.substring(0, prefix.length()).toLowerCase();\r
+               String lcPrefix = prefix.toLowerCase();\r
+               return lcStr.equals(lcPrefix);\r
+       }\r
+\r
+       /**\r
+        * Test if the given String ends with the specified suffix,\r
+        * ignoring upper/lower case.\r
+        * @param str the String to check\r
+        * @param suffix the suffix to look for\r
+        * @see java.lang.String#endsWith\r
+        */\r
+       public static boolean endsWithIgnoreCase(String str, String suffix) {\r
+               if (str == null || suffix == null) {\r
+                       return false;\r
+               }\r
+               if (str.endsWith(suffix)) {\r
+                       return true;\r
+               }\r
+               if (str.length() < suffix.length()) {\r
+                       return false;\r
+               }\r
+\r
+               String lcStr = str.substring(str.length() - suffix.length()).toLowerCase();\r
+               String lcSuffix = suffix.toLowerCase();\r
+               return lcStr.equals(lcSuffix);\r
+       }\r
+\r
+       /**\r
+        * Test whether the given string matches the given substring\r
+        * at the given index.\r
+        * @param str the original string (or StringBuffer)\r
+        * @param index the index in the original string to start matching against\r
+        * @param substring the substring to match at the given index\r
+        */\r
+       public static boolean substringMatch(CharSequence str, int index, CharSequence substring) {\r
+               for (int j = 0; j < substring.length(); j++) {\r
+                       int i = index + j;\r
+                       if (i >= str.length() || str.charAt(i) != substring.charAt(j)) {\r
+                               return false;\r
+                       }\r
+               }\r
+               return true;\r
+       }\r
+\r
+       /**\r
+        * Count the occurrences of the substring in string s.\r
+        * @param str string to search in. Return 0 if this is null.\r
+        * @param sub string to search for. Return 0 if this is null.\r
+        */\r
+       public static int countOccurrencesOf(String str, String sub) {\r
+               if (str == null || sub == null || str.length() == 0 || sub.length() == 0) {\r
+                       return 0;\r
+               }\r
+               int count = 0, pos = 0, idx = 0;\r
+               while ((idx = str.indexOf(sub, pos)) != -1) {\r
+                       ++count;\r
+                       pos = idx + sub.length();\r
+               }\r
+               return count;\r
+       }\r
+\r
+       /**\r
+        * Replace all occurences of a substring within a string with\r
+        * another string.\r
+        * @param inString String to examine\r
+        * @param oldPattern String to replace\r
+        * @param newPattern String to insert\r
+        * @return a String with the replacements\r
+        */\r
+       public static String replace(String inString, String oldPattern, String newPattern) {\r
+               if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) {\r
+                       return inString;\r
+               }\r
+               StringBuffer sbuf = new StringBuffer();\r
+               // output StringBuffer we'll build up\r
+               int pos = 0; // our position in the old string\r
+               int index = inString.indexOf(oldPattern);\r
+               // the index of an occurrence we've found, or -1\r
+               int patLen = oldPattern.length();\r
+               while (index >= 0) {\r
+                       sbuf.append(inString.substring(pos, index));\r
+                       sbuf.append(newPattern);\r
+                       pos = index + patLen;\r
+                       index = inString.indexOf(oldPattern, pos);\r
+               }\r
+               sbuf.append(inString.substring(pos));\r
+               // remember to append any characters to the right of a match\r
+               return sbuf.toString();\r
+       }\r
+\r
+       /**\r
+        * Delete all occurrences of the given substring.\r
+        * @param inString the original String\r
+        * @param pattern the pattern to delete all occurrences of\r
+        * @return the resulting String\r
+        */\r
+       public static String delete(String inString, String pattern) {\r
+               return replace(inString, pattern, "");\r
+       }\r
+\r
+       /**\r
+        * Delete any character in a given String.\r
+        * @param inString the original String\r
+        * @param charsToDelete a set of characters to delete.\r
+        * E.g. "az\n" will delete 'a's, 'z's and new lines.\r
+        * @return the resulting String\r
+        */\r
+       public static String deleteAny(String inString, String charsToDelete) {\r
+               if (!hasLength(inString) || !hasLength(charsToDelete)) {\r
+                       return inString;\r
+               }\r
+               StringBuffer out = new StringBuffer();\r
+               for (int i = 0; i < inString.length(); i++) {\r
+                       char c = inString.charAt(i);\r
+                       if (charsToDelete.indexOf(c) == -1) {\r
+                               out.append(c);\r
+                       }\r
+               }\r
+               return out.toString();\r
+       }\r
+\r
+\r
+       //---------------------------------------------------------------------\r
+       // Convenience methods for working with formatted Strings\r
+       //---------------------------------------------------------------------\r
+\r
+       /**\r
+        * Quote the given String with single quotes.\r
+        * @param str the input String (e.g. "myString")\r
+        * @return the quoted String (e.g. "'myString'"),\r
+        * or <code>null</code> if the input was <code>null</code>\r
+        */\r
+       public static String quote(String str) {\r
+               return (str != null ? "'" + str + "'" : null);\r
+       }\r
+\r
+       /**\r
+        * Turn the given Object into a String with single quotes\r
+        * if it is a String; keeping the Object as-is else.\r
+        * @param obj the input Object (e.g. "myString")\r
+        * @return the quoted String (e.g. "'myString'"),\r
+        * or the input object as-is if not a String\r
+        */\r
+       public static Object quoteIfString(Object obj) {\r
+               return (obj instanceof String ? quote((String) obj) : obj);\r
+       }\r
+\r
+       /**\r
+        * Unqualify a string qualified by a '.' dot character. For example,\r
+        * "this.name.is.qualified", returns "qualified".\r
+        * @param qualifiedName the qualified name\r
+        */\r
+       public static String unqualify(String qualifiedName) {\r
+               return unqualify(qualifiedName, '.');\r
+       }\r
+\r
+       /**\r
+        * Unqualify a string qualified by a separator character. For example,\r
+        * "this:name:is:qualified" returns "qualified" if using a ':' separator.\r
+        * @param qualifiedName the qualified name\r
+        * @param separator the separator\r
+        */\r
+       public static String unqualify(String qualifiedName, char separator) {\r
+               return qualifiedName.substring(qualifiedName.lastIndexOf(separator) + 1);\r
+       }\r
+\r
+       /**\r
+        * Capitalize a <code>String</code>, changing the first letter to\r
+        * upper case as per {@link Character#toUpperCase(char)}.\r
+        * No other letters are changed.\r
+        * @param str the String to capitalize, may be <code>null</code>\r
+        * @return the capitalized String, <code>null</code> if null\r
+        */\r
+       public static String capitalize(String str) {\r
+               return changeFirstCharacterCase(str, true);\r
+       }\r
+\r
+       /**\r
+        * Uncapitalize a <code>String</code>, changing the first letter to\r
+        * lower case as per {@link Character#toLowerCase(char)}.\r
+        * No other letters are changed.\r
+        * @param str the String to uncapitalize, may be <code>null</code>\r
+        * @return the uncapitalized String, <code>null</code> if null\r
+        */\r
+       public static String uncapitalize(String str) {\r
+               return changeFirstCharacterCase(str, false);\r
+       }\r
+\r
+       private static String changeFirstCharacterCase(String str, boolean capitalize) {\r
+               if (str == null || str.length() == 0) {\r
+                       return str;\r
+               }\r
+               StringBuffer buf = new StringBuffer(str.length());\r
+               if (capitalize) {\r
+                       buf.append(Character.toUpperCase(str.charAt(0)));\r
+               }\r
+               else {\r
+                       buf.append(Character.toLowerCase(str.charAt(0)));\r
+               }\r
+               buf.append(str.substring(1));\r
+               return buf.toString();\r
+       }\r
+\r
+       /**\r
+        * Extract the filename from the given path,\r
+        * e.g. "mypath/myfile.txt" to "myfile.txt".\r
+        * @param path the file path (may be <code>null</code>)\r
+        * @return the extracted filename, or <code>null</code> if none\r
+        */\r
+       public static String getFilename(String path) {\r
+               if (path == null) {\r
+                       return null;\r
+               }\r
+               int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR);\r
+               return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path);\r
+       }\r
+\r
+       /**\r
+        * Extract the filename extension from the given path,\r
+        * e.g. "mypath/myfile.txt" to "txt".\r
+        * @param path the file path (may be <code>null</code>)\r
+        * @return the extracted filename extension, or <code>null</code> if none\r
+        */\r
+       public static String getFilenameExtension(String path) {\r
+               if (path == null) {\r
+                       return null;\r
+               }\r
+               int sepIndex = path.lastIndexOf(EXTENSION_SEPARATOR);\r
+               return (sepIndex != -1 ? path.substring(sepIndex + 1) : null);\r
+       }\r
+\r
+       /**\r
+        * Strip the filename extension from the given path,\r
+        * e.g. "mypath/myfile.txt" to "mypath/myfile".\r
+        * @param path the file path (may be <code>null</code>)\r
+        * @return the path with stripped filename extension,\r
+        * or <code>null</code> if none\r
+        */\r
+       public static String stripFilenameExtension(String path) {\r
+               if (path == null) {\r
+                       return null;\r
+               }\r
+               int sepIndex = path.lastIndexOf(EXTENSION_SEPARATOR);\r
+               return (sepIndex != -1 ? path.substring(0, sepIndex) : path);\r
+       }\r
+\r
+       /**\r
+        * Apply the given relative path to the given path,\r
+        * assuming standard Java folder separation (i.e. "/" separators);\r
+        * @param path the path to start from (usually a full file path)\r
+        * @param relativePath the relative path to apply\r
+        * (relative to the full file path above)\r
+        * @return the full file path that results from applying the relative path\r
+        */\r
+       public static String applyRelativePath(String path, String relativePath) {\r
+               int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR);\r
+               if (separatorIndex != -1) {\r
+                       String newPath = path.substring(0, separatorIndex);\r
+                       if (!relativePath.startsWith(FOLDER_SEPARATOR)) {\r
+                               newPath += FOLDER_SEPARATOR;\r
+                       }\r
+                       return newPath + relativePath;\r
+               }\r
+               else {\r
+                       return relativePath;\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Normalize the path by suppressing sequences like "path/.." and\r
+        * inner simple dots.\r
+        * <p>The result is convenient for path comparison. For other uses,\r
+        * notice that Windows separators ("\") are replaced by simple slashes.\r
+        * @param path the original path\r
+        * @return the normalized path\r
+        */\r
+       public static String cleanPath(String path) {\r
+               if (path == null) {\r
+                       return null;\r
+               }\r
+               String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);\r
+\r
+               // Strip prefix from path to analyze, to not treat it as part of the\r
+               // first path element. This is necessary to correctly parse paths like\r
+               // "file:core/../core/io/Resource.class", where the ".." should just\r
+               // strip the first "core" directory while keeping the "file:" prefix.\r
+               int prefixIndex = pathToUse.indexOf(":");\r
+               String prefix = "";\r
+               if (prefixIndex != -1) {\r
+                       prefix = pathToUse.substring(0, prefixIndex + 1);\r
+                       pathToUse = pathToUse.substring(prefixIndex + 1);\r
+               }\r
+               if (pathToUse.startsWith(FOLDER_SEPARATOR)) {\r
+                       prefix = prefix + FOLDER_SEPARATOR;\r
+                       pathToUse = pathToUse.substring(1);\r
+               }\r
+\r
+               String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR);\r
+               List pathElements = new LinkedList();\r
+               int tops = 0;\r
+\r
+               for (int i = pathArray.length - 1; i >= 0; i--) {\r
+                       String element = pathArray[i];\r
+                       if (CURRENT_PATH.equals(element)) {\r
+                               // Points to current directory - drop it.\r
+                       }\r
+                       else if (TOP_PATH.equals(element)) {\r
+                               // Registering top path found.\r
+                               tops++;\r
+                       }\r
+                       else {\r
+                               if (tops > 0) {\r
+                                       // Merging path element with element corresponding to top path.\r
+                                       tops--;\r
+                               }\r
+                               else {\r
+                                       // Normal path element found.\r
+                                       pathElements.add(0, element);\r
+                               }\r
+                       }\r
+               }\r
+\r
+               // Remaining top paths need to be retained.\r
+               for (int i = 0; i < tops; i++) {\r
+                       pathElements.add(0, TOP_PATH);\r
+               }\r
+\r
+               return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR);\r
+       }\r
+\r
+       /**\r
+        * Compare two paths after normalization of them.\r
+        * @param path1 first path for comparison\r
+        * @param path2 second path for comparison\r
+        * @return whether the two paths are equivalent after normalization\r
+        */\r
+       public static boolean pathEquals(String path1, String path2) {\r
+               return cleanPath(path1).equals(cleanPath(path2));\r
+       }\r
+\r
+       /**\r
+        * Parse the given <code>localeString</code> into a {@link Locale}.\r
+        * <p>This is the inverse operation of {@link Locale#toString Locale's toString}.\r
+        * @param localeString the locale string, following <code>Locale's</code>\r
+        * <code>toString()</code> format ("en", "en_UK", etc);\r
+        * also accepts spaces as separators, as an alternative to underscores\r
+        * @return a corresponding <code>Locale</code> instance\r
+        */\r
+       public static Locale parseLocaleString(String localeString) {\r
+               String[] parts = tokenizeToStringArray(localeString, "_ ", false, false);\r
+               String language = (parts.length > 0 ? parts[0] : "");\r
+               String country = (parts.length > 1 ? parts[1] : "");\r
+               String variant = "";\r
+               if (parts.length >= 2) {\r
+                       // There is definitely a variant, and it is everything after the country\r
+                       // code sans the separator between the country code and the variant.\r
+                       int endIndexOfCountryCode = localeString.indexOf(country) + country.length();\r
+                       // Strip off any leading '_' and whitespace, what's left is the variant.\r
+                       variant = trimLeadingWhitespace(localeString.substring(endIndexOfCountryCode));\r
+                       if (variant.startsWith("_")) {\r
+                               variant = trimLeadingCharacter(variant, '_');\r
+                       }\r
+               }\r
+               return (language.length() > 0 ? new Locale(language, country, variant) : null);\r
+       }\r
+\r
+       /**\r
+        * Determine the RFC 3066 compliant language tag,\r
+        * as used for the HTTP "Accept-Language" header.\r
+        * @param locale the Locale to transform to a language tag\r
+        * @return the RFC 3066 compliant language tag as String\r
+        */\r
+       public static String toLanguageTag(Locale locale) {\r
+               return locale.getLanguage() + (hasText(locale.getCountry()) ? "-" + locale.getCountry() : "");\r
+       }\r
+\r
+\r
+       //---------------------------------------------------------------------\r
+       // Convenience methods for working with String arrays\r
+       //---------------------------------------------------------------------\r
+\r
+       /**\r
+        * Append the given String to the given String array, returning a new array\r
+        * consisting of the input array contents plus the given String.\r
+        * @param array the array to append to (can be <code>null</code>)\r
+        * @param str the String to append\r
+        * @return the new array (never <code>null</code>)\r
+        */\r
+       public static String[] addStringToArray(String[] array, String str) {\r
+               if (ObjectUtils.isEmpty(array)) {\r
+                       return new String[] {str};\r
+               }\r
+               String[] newArr = new String[array.length + 1];\r
+               System.arraycopy(array, 0, newArr, 0, array.length);\r
+               newArr[array.length] = str;\r
+               return newArr;\r
+       }\r
+\r
+       /**\r
+        * Concatenate the given String arrays into one,\r
+        * with overlapping array elements included twice.\r
+        * <p>The order of elements in the original arrays is preserved.\r
+        * @param array1 the first array (can be <code>null</code>)\r
+        * @param array2 the second array (can be <code>null</code>)\r
+        * @return the new array (<code>null</code> if both given arrays were <code>null</code>)\r
+        */\r
+       public static String[] concatenateStringArrays(String[] array1, String[] array2) {\r
+               if (ObjectUtils.isEmpty(array1)) {\r
+                       return array2;\r
+               }\r
+               if (ObjectUtils.isEmpty(array2)) {\r
+                       return array1;\r
+               }\r
+               String[] newArr = new String[array1.length + array2.length];\r
+               System.arraycopy(array1, 0, newArr, 0, array1.length);\r
+               System.arraycopy(array2, 0, newArr, array1.length, array2.length);\r
+               return newArr;\r
+       }\r
+\r
+       /**\r
+        * Merge the given String arrays into one, with overlapping\r
+        * array elements only included once.\r
+        * <p>The order of elements in the original arrays is preserved\r
+        * (with the exception of overlapping elements, which are only\r
+        * included on their first occurence).\r
+        * @param array1 the first array (can be <code>null</code>)\r
+        * @param array2 the second array (can be <code>null</code>)\r
+        * @return the new array (<code>null</code> if both given arrays were <code>null</code>)\r
+        */\r
+       public static String[] mergeStringArrays(String[] array1, String[] array2) {\r
+               if (ObjectUtils.isEmpty(array1)) {\r
+                       return array2;\r
+               }\r
+               if (ObjectUtils.isEmpty(array2)) {\r
+                       return array1;\r
+               }\r
+               List result = new ArrayList();\r
+               result.addAll(Arrays.asList(array1));\r
+               for (int i = 0; i < array2.length; i++) {\r
+                       String str = array2[i];\r
+                       if (!result.contains(str)) {\r
+                               result.add(str);\r
+                       }\r
+               }\r
+               return toStringArray(result);\r
+       }\r
+\r
+       /**\r
+        * Turn given source String array into sorted array.\r
+        * @param array the source array\r
+        * @return the sorted array (never <code>null</code>)\r
+        */\r
+       public static String[] sortStringArray(String[] array) {\r
+               if (ObjectUtils.isEmpty(array)) {\r
+                       return new String[0];\r
+               }\r
+               Arrays.sort(array);\r
+               return array;\r
+       }\r
+\r
+       /**\r
+        * Copy the given Collection into a String array.\r
+        * The Collection must contain String elements only.\r
+        * @param collection the Collection to copy\r
+        * @return the String array (<code>null</code> if the passed-in\r
+        * Collection was <code>null</code>)\r
+        */\r
+       public static String[] toStringArray(Collection collection) {\r
+               if (collection == null) {\r
+                       return null;\r
+               }\r
+               return (String[]) collection.toArray(new String[collection.size()]);\r
+       }\r
+\r
+       /**\r
+        * Copy the given Enumeration into a String array.\r
+        * The Enumeration must contain String elements only.\r
+        * @param enumeration the Enumeration to copy\r
+        * @return the String array (<code>null</code> if the passed-in\r
+        * Enumeration was <code>null</code>)\r
+        */\r
+       public static String[] toStringArray(Enumeration enumeration) {\r
+               if (enumeration == null) {\r
+                       return null;\r
+               }\r
+               List list = Collections.list(enumeration);\r
+               return (String[]) list.toArray(new String[list.size()]);\r
+       }\r
+\r
+       /**\r
+        * Trim the elements of the given String array,\r
+        * calling <code>String.trim()</code> on each of them.\r
+        * @param array the original String array\r
+        * @return the resulting array (of the same size) with trimmed elements\r
+        */\r
+       public static String[] trimArrayElements(String[] array) {\r
+               if (ObjectUtils.isEmpty(array)) {\r
+                       return new String[0];\r
+               }\r
+               String[] result = new String[array.length];\r
+               for (int i = 0; i < array.length; i++) {\r
+                       String element = array[i];\r
+                       result[i] = (element != null ? element.trim() : null);\r
+               }\r
+               return result;\r
+       }\r
+\r
+       /**\r
+        * Remove duplicate Strings from the given array.\r
+        * Also sorts the array, as it uses a TreeSet.\r
+        * @param array the String array\r
+        * @return an array without duplicates, in natural sort order\r
+        */\r
+       public static String[] removeDuplicateStrings(String[] array) {\r
+               if (ObjectUtils.isEmpty(array)) {\r
+                       return array;\r
+               }\r
+               Set set = new TreeSet();\r
+               for (int i = 0; i < array.length; i++) {\r
+                       set.add(array[i]);\r
+               }\r
+               return toStringArray(set);\r
+       }\r
+\r
+       /**\r
+        * Split a String at the first occurrence of the delimiter.\r
+        * Does not include the delimiter in the result.\r
+        * @param toSplit the string to split\r
+        * @param delimiter to split the string up with\r
+        * @return a two element array with index 0 being before the delimiter, and\r
+        * index 1 being after the delimiter (neither element includes the delimiter);\r
+        * or <code>null</code> if the delimiter wasn't found in the given input String\r
+        */\r
+       public static String[] split(String toSplit, String delimiter) {\r
+               if (!hasLength(toSplit) || !hasLength(delimiter)) {\r
+                       return null;\r
+               }\r
+               int offset = toSplit.indexOf(delimiter);\r
+               if (offset < 0) {\r
+                       return null;\r
+               }\r
+               String beforeDelimiter = toSplit.substring(0, offset);\r
+               String afterDelimiter = toSplit.substring(offset + delimiter.length());\r
+               return new String[] {beforeDelimiter, afterDelimiter};\r
+       }\r
+\r
+       /**\r
+        * Take an array Strings and split each element based on the given delimiter.\r
+        * A <code>Properties</code> instance is then generated, with the left of the\r
+        * delimiter providing the key, and the right of the delimiter providing the value.\r
+        * <p>Will trim both the key and value before adding them to the\r
+        * <code>Properties</code> instance.\r
+        * @param array the array to process\r
+        * @param delimiter to split each element using (typically the equals symbol)\r
+        * @return a <code>Properties</code> instance representing the array contents,\r
+        * or <code>null</code> if the array to process was null or empty\r
+        */\r
+       public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter) {\r
+               return splitArrayElementsIntoProperties(array, delimiter, null);\r
+       }\r
+\r
+       /**\r
+        * Take an array Strings and split each element based on the given delimiter.\r
+        * A <code>Properties</code> instance is then generated, with the left of the\r
+        * delimiter providing the key, and the right of the delimiter providing the value.\r
+        * <p>Will trim both the key and value before adding them to the\r
+        * <code>Properties</code> instance.\r
+        * @param array the array to process\r
+        * @param delimiter to split each element using (typically the equals symbol)\r
+        * @param charsToDelete one or more characters to remove from each element\r
+        * prior to attempting the split operation (typically the quotation mark\r
+        * symbol), or <code>null</code> if no removal should occur\r
+        * @return a <code>Properties</code> instance representing the array contents,\r
+        * or <code>null</code> if the array to process was <code>null</code> or empty\r
+        */\r
+       public static Properties splitArrayElementsIntoProperties(\r
+                       String[] array, String delimiter, String charsToDelete) {\r
+\r
+               if (ObjectUtils.isEmpty(array)) {\r
+                       return null;\r
+               }\r
+               Properties result = new Properties();\r
+               for (int i = 0; i < array.length; i++) {\r
+                       String element = array[i];\r
+                       if (charsToDelete != null) {\r
+                               element = deleteAny(array[i], charsToDelete);\r
+                       }\r
+                       String[] splittedElement = split(element, delimiter);\r
+                       if (splittedElement == null) {\r
+                               continue;\r
+                       }\r
+                       result.setProperty(splittedElement[0].trim(), splittedElement[1].trim());\r
+               }\r
+               return result;\r
+       }\r
+\r
+       /**\r
+        * Tokenize the given String into a String array via a StringTokenizer.\r
+        * Trims tokens and omits empty tokens.\r
+        * <p>The given delimiters string is supposed to consist of any number of\r
+        * delimiter characters. Each of those characters can be used to separate\r
+        * tokens. A delimiter is always a single character; for multi-character\r
+        * delimiters, consider using <code>delimitedListToStringArray</code>\r
+        * @param str the String to tokenize\r
+        * @param delimiters the delimiter characters, assembled as String\r
+        * (each of those characters is individually considered as delimiter).\r
+        * @return an array of the tokens\r
+        * @see java.util.StringTokenizer\r
+        * @see java.lang.String#trim()\r
+        * @see #delimitedListToStringArray\r
+        */\r
+       public static String[] tokenizeToStringArray(String str, String delimiters) {\r
+               return tokenizeToStringArray(str, delimiters, true, true);\r
+       }\r
+\r
+       /**\r
+        * Tokenize the given String into a String array via a StringTokenizer.\r
+        * <p>The given delimiters string is supposed to consist of any number of\r
+        * delimiter characters. Each of those characters can be used to separate\r
+        * tokens. A delimiter is always a single character; for multi-character\r
+        * delimiters, consider using <code>delimitedListToStringArray</code>\r
+        * @param str the String to tokenize\r
+        * @param delimiters the delimiter characters, assembled as String\r
+        * (each of those characters is individually considered as delimiter)\r
+        * @param trimTokens trim the tokens via String's <code>trim</code>\r
+        * @param ignoreEmptyTokens omit empty tokens from the result array\r
+        * (only applies to tokens that are empty after trimming; StringTokenizer\r
+        * will not consider subsequent delimiters as token in the first place).\r
+        * @return an array of the tokens (<code>null</code> if the input String\r
+        * was <code>null</code>)\r
+        * @see java.util.StringTokenizer\r
+        * @see java.lang.String#trim()\r
+        * @see #delimitedListToStringArray\r
+        */\r
+       public static String[] tokenizeToStringArray(\r
+                       String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) {\r
+\r
+               if (str == null) {\r
+                       return null;\r
+               }\r
+               StringTokenizer st = new StringTokenizer(str, delimiters);\r
+               List tokens = new ArrayList();\r
+               while (st.hasMoreTokens()) {\r
+                       String token = st.nextToken();\r
+                       if (trimTokens) {\r
+                               token = token.trim();\r
+                       }\r
+                       if (!ignoreEmptyTokens || token.length() > 0) {\r
+                               tokens.add(token);\r
+                       }\r
+               }\r
+               return toStringArray(tokens);\r
+       }\r
+\r
+       /**\r
+        * Take a String which is a delimited list and convert it to a String array.\r
+        * <p>A single delimiter can consists of more than one character: It will still\r
+        * be considered as single delimiter string, rather than as bunch of potential\r
+        * delimiter characters - in contrast to <code>tokenizeToStringArray</code>.\r
+        * @param str the input String\r
+        * @param delimiter the delimiter between elements (this is a single delimiter,\r
+        * rather than a bunch individual delimiter characters)\r
+        * @return an array of the tokens in the list\r
+        * @see #tokenizeToStringArray\r
+        */\r
+       public static String[] delimitedListToStringArray(String str, String delimiter) {\r
+               return delimitedListToStringArray(str, delimiter, null);\r
+       }\r
+\r
+       /**\r
+        * Take a String which is a delimited list and convert it to a String array.\r
+        * <p>A single delimiter can consists of more than one character: It will still\r
+        * be considered as single delimiter string, rather than as bunch of potential\r
+        * delimiter characters - in contrast to <code>tokenizeToStringArray</code>.\r
+        * @param str the input String\r
+        * @param delimiter the delimiter between elements (this is a single delimiter,\r
+        * rather than a bunch individual delimiter characters)\r
+        * @param charsToDelete a set of characters to delete. Useful for deleting unwanted\r
+        * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a String.\r
+        * @return an array of the tokens in the list\r
+        * @see #tokenizeToStringArray\r
+        */\r
+       public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete) {\r
+               if (str == null) {\r
+                       return new String[0];\r
+               }\r
+               if (delimiter == null) {\r
+                       return new String[] {str};\r
+               }\r
+               List result = new ArrayList();\r
+               if ("".equals(delimiter)) {\r
+                       for (int i = 0; i < str.length(); i++) {\r
+                               result.add(deleteAny(str.substring(i, i + 1), charsToDelete));\r
+                       }\r
+               }\r
+               else {\r
+                       int pos = 0;\r
+                       int delPos = 0;\r
+                       while ((delPos = str.indexOf(delimiter, pos)) != -1) {\r
+                               result.add(deleteAny(str.substring(pos, delPos), charsToDelete));\r
+                               pos = delPos + delimiter.length();\r
+                       }\r
+                       if (str.length() > 0 && pos <= str.length()) {\r
+                               // Add rest of String, but not in case of empty input.\r
+                               result.add(deleteAny(str.substring(pos), charsToDelete));\r
+                       }\r
+               }\r
+               return toStringArray(result);\r
+       }\r
+\r
+       /**\r
+        * Convert a CSV list into an array of Strings.\r
+        * @param str the input String\r
+        * @return an array of Strings, or the empty array in case of empty input\r
+        */\r
+       public static String[] commaDelimitedListToStringArray(String str) {\r
+               return delimitedListToStringArray(str, ",");\r
+       }\r
+\r
+       /**\r
+        * Convenience method to convert a CSV string list to a set.\r
+        * Note that this will suppress duplicates.\r
+        * @param str the input String\r
+        * @return a Set of String entries in the list\r
+        */\r
+       public static Set commaDelimitedListToSet(String str) {\r
+               Set set = new TreeSet();\r
+               String[] tokens = commaDelimitedListToStringArray(str);\r
+               for (int i = 0; i < tokens.length; i++) {\r
+                       set.add(tokens[i]);\r
+               }\r
+               return set;\r
+       }\r
+\r
+       /**\r
+        * Convenience method to return a Collection as a delimited (e.g. CSV)\r
+        * String. E.g. useful for <code>toString()</code> implementations.\r
+        * @param coll the Collection to display\r
+        * @param delim the delimiter to use (probably a ",")\r
+        * @param prefix the String to start each element with\r
+        * @param suffix the String to end each element with\r
+        * @return the delimited String\r
+        */\r
+       public static String collectionToDelimitedString(Collection coll, String delim, String prefix, String suffix) {\r
+               if (CollectionUtils.isEmpty(coll)) {\r
+                       return "";\r
+               }\r
+               StringBuffer sb = new StringBuffer();\r
+               Iterator it = coll.iterator();\r
+               while (it.hasNext()) {\r
+                       sb.append(prefix).append(it.next()).append(suffix);\r
+                       if (it.hasNext()) {\r
+                               sb.append(delim);\r
+                       }\r
+               }\r
+               return sb.toString();\r
+       }\r
+\r
+       /**\r
+        * Convenience method to return a Collection as a delimited (e.g. CSV)\r
+        * String. E.g. useful for <code>toString()</code> implementations.\r
+        * @param coll the Collection to display\r
+        * @param delim the delimiter to use (probably a ",")\r
+        * @return the delimited String\r
+        */\r
+       public static String collectionToDelimitedString(Collection coll, String delim) {\r
+               return collectionToDelimitedString(coll, delim, "", "");\r
+       }\r
+\r
+       /**\r
+        * Convenience method to return a Collection as a CSV String.\r
+        * E.g. useful for <code>toString()</code> implementations.\r
+        * @param coll the Collection to display\r
+        * @return the delimited String\r
+        */\r
+       public static String collectionToCommaDelimitedString(Collection coll) {\r
+               return collectionToDelimitedString(coll, ",");\r
+       }\r
+\r
+       /**\r
+        * Convenience method to return a String array as a delimited (e.g. CSV)\r
+        * String. E.g. useful for <code>toString()</code> implementations.\r
+        * @param arr the array to display\r
+        * @param delim the delimiter to use (probably a ",")\r
+        * @return the delimited String\r
+        */\r
+       public static String arrayToDelimitedString(Object[] arr, String delim) {\r
+               if (ObjectUtils.isEmpty(arr)) {\r
+                       return "";\r
+               }\r
+               StringBuffer sb = new StringBuffer();\r
+               for (int i = 0; i < arr.length; i++) {\r
+                       if (i > 0) {\r
+                               sb.append(delim);\r
+                       }\r
+                       sb.append(arr[i]);\r
+               }\r
+               return sb.toString();\r
+       }\r
+\r
+       /**\r
+        * Convenience method to return a String array as a CSV String.\r
+        * E.g. useful for <code>toString()</code> implementations.\r
+        * @param arr the array to display\r
+        * @return the delimited String\r
+        */\r
+       public static String arrayToCommaDelimitedString(Object[] arr) {\r
+               return arrayToDelimitedString(arr, ",");\r
+       }\r
+\r
+}\r
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/SystemPropertyUtils.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/internal/springutil/SystemPropertyUtils.java
new file mode 100644 (file)
index 0000000..ff81a22
--- /dev/null
@@ -0,0 +1,88 @@
+/*\r
+ * Copyright 2002-2008 the original author or authors.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package org.argeo.osgi.boot.internal.springutil;\r
+\r
+/**\r
+ * Helper class for resolving placeholders in texts. Usually applied to file paths.\r
+ *\r
+ * <p>A text may contain <code>${...}</code> placeholders, to be resolved as\r
+ * system properties: e.g. <code>${user.dir}</code>.\r
+ *\r
+ * @author Juergen Hoeller\r
+ * @since 1.2.5\r
+ * @see #PLACEHOLDER_PREFIX\r
+ * @see #PLACEHOLDER_SUFFIX\r
+ * @see System#getProperty(String)\r
+ */\r
+public abstract class SystemPropertyUtils {\r
+\r
+       /** Prefix for system property placeholders: "${" */\r
+       public static final String PLACEHOLDER_PREFIX = "${";\r
+\r
+       /** Suffix for system property placeholders: "}" */\r
+       public static final String PLACEHOLDER_SUFFIX = "}";\r
+\r
+\r
+       /**\r
+        * Resolve ${...} placeholders in the given text,\r
+        * replacing them with corresponding system property values.\r
+        * @param text the String to resolve\r
+        * @return the resolved String\r
+        * @see #PLACEHOLDER_PREFIX\r
+        * @see #PLACEHOLDER_SUFFIX\r
+        */\r
+       @SuppressWarnings("unused")\r
+       public static String resolvePlaceholders(String text) {\r
+               StringBuffer buf = new StringBuffer(text);\r
+\r
+               int startIndex = buf.indexOf(PLACEHOLDER_PREFIX);\r
+               while (startIndex != -1) {\r
+                       int endIndex = buf.indexOf(PLACEHOLDER_SUFFIX, startIndex + PLACEHOLDER_PREFIX.length());\r
+                       if (endIndex != -1) {\r
+                               String placeholder = buf.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);\r
+                               int nextIndex = endIndex + PLACEHOLDER_SUFFIX.length();\r
+                               try {\r
+                                       String propVal = System.getProperty(placeholder);\r
+                                       if (propVal == null) {\r
+                                               // Fall back to searching the system environment.\r
+                                               //propVal = System.getenv(placeholder);// mbaudier - 2009-07-26\r
+                                               throw new Error("getenv no longer supported, use properties and -D instead: " + placeholder);\r
+                                       }\r
+                                       if (propVal != null) {\r
+                                               buf.replace(startIndex, endIndex + PLACEHOLDER_SUFFIX.length(), propVal);\r
+                                               nextIndex = startIndex + propVal.length();\r
+                                       }\r
+                                       else {\r
+                                               System.err.println("Could not resolve placeholder '" + placeholder + "' in [" + text +\r
+                                                               "] as system property: neither system property nor environment variable found");\r
+                                       }\r
+                               }\r
+                               catch (Throwable ex) {\r
+                                       System.err.println("Could not resolve placeholder '" + placeholder + "' in [" + text +\r
+                                                       "] as system property: " + ex);\r
+                               }\r
+                               startIndex = buf.indexOf(PLACEHOLDER_PREFIX, nextIndex);\r
+                       }\r
+                       else {\r
+                               startIndex = -1;\r
+                       }\r
+               }\r
+\r
+               return buf.toString();\r
+       }\r
+\r
+}\r
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/log4j.properties b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/log4j.properties
new file mode 100644 (file)
index 0000000..1fcf25e
--- /dev/null
@@ -0,0 +1,12 @@
+log4j.rootLogger=WARN, console
+
+log4j.logger.org.argeo=INFO
+
+## Appenders
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern= %-5p %d{ISO8601} %m - %c - [%t]%n
+
+log4j.appender.development=org.apache.log4j.ConsoleAppender
+log4j.appender.development.layout=org.apache.log4j.PatternLayout
+log4j.appender.development.layout.ConversionPattern=%d{ABSOLUTE} %m (%F:%L) [%t] %p %n
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/node.policy b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/node.policy
new file mode 100644 (file)
index 0000000..facb613
--- /dev/null
@@ -0,0 +1,3 @@
+grant {
+  permission java.security.AllPermission;
+};
\ No newline at end of file
diff --git a/org.argeo.util/.classpath b/org.argeo.util/.classpath
new file mode 100644 (file)
index 0000000..4e5da1d
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" path="src" />
+       <classpathentry kind="src" path="ext/test" />
+       <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-1.8" />
+       <classpathentry kind="output" path="bin" />
+</classpath>
diff --git a/org.argeo.util/.gitignore b/org.argeo.util/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/org.argeo.util/.project b/org.argeo.util/.project
new file mode 100644 (file)
index 0000000..171ff88
--- /dev/null
@@ -0,0 +1,28 @@
+<?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
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/org.argeo.util/bnd.bnd b/org.argeo.util/bnd.bnd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/org.argeo.util/build.properties b/org.argeo.util/build.properties
new file mode 100644 (file)
index 0000000..9a8006a
--- /dev/null
@@ -0,0 +1,6 @@
+source.. = src/,\
+           ext/test/
+output.. = bin/
+additional.bundles = org.junit,\
+                     org.slf4j.commons.logging,\
+                     org.apache.log4j
diff --git a/org.argeo.util/ext/test/org/argeo/util/CsvParserEncodingTestCase.java b/org.argeo.util/ext/test/org/argeo/util/CsvParserEncodingTestCase.java
new file mode 100644 (file)
index 0000000..111a873
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+public class CsvParserEncodingTestCase extends TestCase {
+
+       private String iso = "ISO-8859-1";
+       private String utf8 = "UTF-8";
+
+       public void testParse() throws Exception {
+
+               String xml = new String("áéíóúñ,éééé");
+               byte[] utfBytes = xml.getBytes(utf8);
+               byte[] isoBytes = xml.getBytes(iso);
+
+               InputStream inUtf = new ByteArrayInputStream(utfBytes);
+               InputStream inIso = new ByteArrayInputStream(isoBytes);
+
+               CsvParser csvParser = new CsvParser() {
+                       protected void processLine(Integer lineNumber, List<String> header,
+                                       List<String> tokens) {
+                               assertEquals(header.size(), tokens.size());
+                               assertEquals(2, tokens.size());
+                               assertEquals("áéíóúñ", tokens.get(0));
+                               assertEquals("éééé", tokens.get(1));
+                       }
+               };
+
+               csvParser.parse(inUtf, utf8);
+               inUtf.close();
+               csvParser.parse(inIso, iso);
+               inIso.close();
+       }
+}
diff --git a/org.argeo.util/ext/test/org/argeo/util/CsvParserParseFileTest.java b/org.argeo.util/ext/test/org/argeo/util/CsvParserParseFileTest.java
new file mode 100644 (file)
index 0000000..a937ee7
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.util;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+public class CsvParserParseFileTest extends TestCase {
+       public void testParse() throws Exception {
+
+               final Map<Integer, Map<String, String>> lines = new HashMap<Integer, Map<String, String>>();
+               InputStream in = getClass().getResourceAsStream(
+                               "/org/argeo/util/ReferenceFile.csv");
+               CsvParserWithLinesAsMap parser = new CsvParserWithLinesAsMap() {
+                       protected void processLine(Integer lineNumber,
+                                       Map<String, String> line) {
+                               lines.put(lineNumber, line);
+                       }
+               };
+
+               parser.parse(in);
+               in.close();
+
+               assertEquals(5, lines.size());
+       }
+
+}
diff --git a/org.argeo.util/ext/test/org/argeo/util/CsvParserTestCase.java b/org.argeo.util/ext/test/org/argeo/util/CsvParserTestCase.java
new file mode 100644 (file)
index 0000000..02e8d1b
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+public class CsvParserTestCase extends TestCase {
+       public void testParse() throws Exception {
+               String toParse = "Header1,\"Header\n2\",Header3,\"Header4\"\n"
+                               + "Col1,\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n"
+                               + "Col1,\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n"
+                               + "Col1,\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n";
+
+               InputStream in = new ByteArrayInputStream(toParse.getBytes());
+
+               CsvParser csvParser = new CsvParser() {
+                       protected void processLine(Integer lineNumber, List<String> header,
+                                       List<String> tokens) {
+                               assertEquals(header.size(), tokens.size());
+                               assertEquals(4, tokens.size());
+                               assertEquals("Col1", tokens.get(0));
+                               assertEquals("Col\n2", tokens.get(1));
+                               assertEquals("Col3", tokens.get(2));
+                               assertEquals("\"Col4\"", tokens.get(3));
+                       }
+               };
+
+               csvParser.parse(in);
+               in.close();
+       }
+
+}
diff --git a/org.argeo.util/ext/test/org/argeo/util/CsvParserWithQuotedSeparatorTest.java b/org.argeo.util/ext/test/org/argeo/util/CsvParserWithQuotedSeparatorTest.java
new file mode 100644 (file)
index 0000000..d4131a0
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+public class CsvParserWithQuotedSeparatorTest extends TestCase {
+       public void testSimpleParse() throws Exception {
+               String toParse = "Header1,\"Header2\",Header3,\"Header4\"\n"
+                               + "\"Col1, Col2\",\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n";
+
+               InputStream in = new ByteArrayInputStream(toParse.getBytes());
+
+               CsvParser csvParser = new CsvParser() {
+                       protected void processLine(Integer lineNumber, List<String> header,
+                                       List<String> tokens) {
+                               assertEquals(header.size(), tokens.size());
+                               assertEquals(4, tokens.size());
+                               assertEquals("Col1, Col2", tokens.get(0));
+                       }
+               };
+               // System.out.println(toParse);
+               csvParser.parse(in);
+               in.close();
+
+       }
+
+       public void testParseFile() throws Exception {
+
+               final Map<Integer, Map<String, String>> lines = new HashMap<Integer, Map<String, String>>();
+               InputStream in = getClass().getResourceAsStream(
+                               "/org/argeo/util/ReferenceFile.csv");
+
+               CsvParserWithLinesAsMap parser = new CsvParserWithLinesAsMap() {
+                       protected void processLine(Integer lineNumber,
+                                       Map<String, String> line) {
+                               // System.out.println("processing line #" + lineNumber);
+                               lines.put(lineNumber, line);
+                       }
+               };
+
+               parser.parse(in);
+               in.close();
+
+               Map<String, String> line = lines.get(2);
+               assertEquals(",,,,", line.get("Coma testing"));
+               line = lines.get(3);
+               assertEquals(",, ,,", line.get("Coma testing"));
+               line = lines.get(4);
+               assertEquals("module1, module2", line.get("Coma testing"));
+               line = lines.get(5);
+               assertEquals("module1,module2", line.get("Coma testing"));
+               line = lines.get(6);
+               assertEquals(",module1,module2, \nmodule3, module4",
+                               line.get("Coma testing"));
+               assertEquals(5, lines.size());
+
+       }
+}
diff --git a/org.argeo.util/ext/test/org/argeo/util/CsvWriterTestCase.java b/org.argeo.util/ext/test/org/argeo/util/CsvWriterTestCase.java
new file mode 100644 (file)
index 0000000..26b356d
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+public class CsvWriterTestCase extends TestCase {
+       public void testWrite() throws Exception {
+               ByteArrayOutputStream out = new ByteArrayOutputStream();
+               final CsvWriter csvWriter = new CsvWriter(out);
+
+               String[] header = { "Header1", "Header 2", "Header,3", "Header\n4",
+                               "Header\"5\"" };
+               String[] line1 = { "Value1", "Value 2", "Value,3", "Value\n4",
+                               "Value\"5\"" };
+               csvWriter.writeLine(Arrays.asList(header));
+               csvWriter.writeLine(Arrays.asList(line1));
+
+               String reference = "Header1,Header 2,\"Header,3\",\"Header\n4\",\"Header\"\"5\"\"\"\n"
+                               + "Value1,Value 2,\"Value,3\",\"Value\n4\",\"Value\"\"5\"\"\"\n";
+               String written = new String(out.toByteArray());
+               assertEquals(reference, written);
+               out.close();
+               System.out.println(written);
+
+               final List<String> allTokens = new ArrayList<String>();
+               CsvParser csvParser = new CsvParser() {
+                       protected void processLine(Integer lineNumber, List<String> header,
+                                       List<String> tokens) {
+                               if (lineNumber == 2)
+                                       allTokens.addAll(header);
+                               allTokens.addAll(tokens);
+                       }
+               };
+               ByteArrayInputStream in = new ByteArrayInputStream(written.getBytes());
+               csvParser.parse(in);
+               in.close();
+               List<String> allTokensRef = new ArrayList<String>();
+               allTokensRef.addAll(Arrays.asList(header));
+               allTokensRef.addAll(Arrays.asList(line1));
+
+               assertEquals(allTokensRef.size(), allTokens.size());
+               for (int i = 0; i < allTokensRef.size(); i++)
+                       assertEquals(allTokensRef.get(i), allTokens.get(i));
+       }
+
+}
diff --git a/org.argeo.util/ext/test/org/argeo/util/ReferenceFile.csv b/org.argeo.util/ext/test/org/argeo/util/ReferenceFile.csv
new file mode 100644 (file)
index 0000000..351453d
--- /dev/null
@@ -0,0 +1,37 @@
+"ID","A long Text","Name","Other","Number","Reference","Target","Date","Update","Language","ID Ref","Weird chars","line feeds","after line feed","Empty column","Status comment","Comments","Empty","Coma testing"
+"AK251","Everything & with some line feed 
+ more “some” quote","Marge S.",,78.6,"A1155222221111268515131",,12/12/12,03/12/08,,9821308500721,"%%%ùù","ao","Nothing special",,,"Some very usefull comment",,",,,,"
+"AG254","same","Roger “wallace” Big","15 – JI",78.5,"A1155222221111268515131","next milestone",12/12/12,03/12/08,"_fr (French - France)",9812309500953,"***µ”","a
+
+
+
+
+o","after line feed",,"Do the job",,,",, ,,"
+"FG211","Very long text with some bullets.
+1 first
+2 second
+3. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long","Father & Son","15 – JI",15.4,"A1155222221111268515131","next milestone",12/12/12,03/12/08,"_fr (French - France)",9812309500952,"///","a
+
+
+
+
+
+
+o","module1,module2",,"Be fast",,,"module1, module2"
+"RRT152","Very long text with some bullets.
+1 first
+2 second
+3. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long","Another $$","15 – JI",12.3,"A1155222221111268515131","next milestone",12/12/12,03/12/08,"_fr (French - France)",9812309500950,"---","a
+
+o
+
+
+","module1,module2",,,,,"module1,module2"
+"YU121","Another use case : “blank line”
+
+After the blank.","nothing with brackets( )","15 – JI",15.2,"A1155222221111268515131",,12/12/12,03/12/08,"_fr (French - France)",9812309500925,",;:?./","ao","
+
+
+
+After line feed again",,,,,",module1,module2, 
+module3, module4"
diff --git a/org.argeo.util/ext/test/org/argeo/util/TestParse-ISO.csv b/org.argeo.util/ext/test/org/argeo/util/TestParse-ISO.csv
new file mode 100644 (file)
index 0000000..0bec611
--- /dev/null
@@ -0,0 +1,8 @@
+"Date d'imputation","N° de compte","Code journal","Pièce interne","Pièce externe","Libellé d'écriture","Débit","Crédit","Lettrage","Quantité","Code analytique","Date d'échéance","Date d'imputation origine","Code journal origine","Mode de règlement","Date début de période","Date fin de période"
+26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"3.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"7.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"411OPEN","BQ","BQ01.10",,"Vir Client ",,"2.508,00","A",,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"455100","BQ","BQ01.10",,"Bankomat Raiffeise","250,00",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"512101","BQ","BQ01.10",,"Extrait bancaire 01.10","12.250,55",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"627800","BQ","BQ01.10",,"Envoi de chequier","2,30",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"627800","BQ","BQ01.10",,"Frais d'expedition","5,15",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
diff --git a/org.argeo.util/ext/test/org/argeo/util/TestParse-UTF-8.csv b/org.argeo.util/ext/test/org/argeo/util/TestParse-UTF-8.csv
new file mode 100644 (file)
index 0000000..0bec611
--- /dev/null
@@ -0,0 +1,8 @@
+"Date d'imputation","N° de compte","Code journal","Pièce interne","Pièce externe","Libellé d'écriture","Débit","Crédit","Lettrage","Quantité","Code analytique","Date d'échéance","Date d'imputation origine","Code journal origine","Mode de règlement","Date début de période","Date fin de période"
+26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"3.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"7.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"411OPEN","BQ","BQ01.10",,"Vir Client ",,"2.508,00","A",,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"455100","BQ","BQ01.10",,"Bankomat Raiffeise","250,00",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"512101","BQ","BQ01.10",,"Extrait bancaire 01.10","12.250,55",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"627800","BQ","BQ01.10",,"Envoi de chequier","2,30",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"627800","BQ","BQ01.10",,"Frais d'expedition","5,15",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
diff --git a/org.argeo.util/ext/test/org/argeo/util/ThroughputTest.java b/org.argeo.util/ext/test/org/argeo/util/ThroughputTest.java
new file mode 100644 (file)
index 0000000..fc8007e
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.util;
+
+import junit.framework.TestCase;
+
+public class ThroughputTest extends TestCase {
+       public void testParse() {
+               Throughput t;
+               t = new Throughput("3.54/s");
+               assertEquals(3.54d, t.getValue());
+               assertEquals(Throughput.Unit.s, t.getUnit());
+               assertEquals(282l, (long) t.asMsPeriod());
+
+               t = new Throughput("35698.2569/h");
+               assertEquals(Throughput.Unit.h, t.getUnit());
+               assertEquals(101l, (long) t.asMsPeriod());
+       }
+}
diff --git a/org.argeo.util/pom.xml b/org.argeo.util/pom.xml
new file mode 100644 (file)
index 0000000..31e1194
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.util</artifactId>
+       <name>Commons Utilities</name>
+</project>
\ No newline at end of file
diff --git a/org.argeo.util/src/org/argeo/util/CsvParser.java b/org.argeo.util/src/org/argeo/util/CsvParser.java
new file mode 100644 (file)
index 0000000..d133afd
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+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)
+        */
+       public synchronized void parse(InputStream in) {
+               parse(in, null);
+       }
+
+       /**
+        * Parses the CSV file (stream is closed at the end)
+        */
+       public synchronized void parse(InputStream in, String encoding) {
+               BufferedReader reader = null;
+               Integer lineCount = 0;
+               try {
+                       if (encoding == null)
+                               reader = new BufferedReader(new InputStreamReader(in));
+                       else
+                               reader = new BufferedReader(new InputStreamReader(in, encoding));
+                       List<String> header = null;
+                       if (!noHeader) {
+                               String headerStr = reader.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 = reader.readLine();
+                                       if (headerStr == null)
+                                               break;
+                                       wasInquote = true;
+                               }
+                               header = Collections.unmodifiableList(header);
+                       }
+
+                       String line = null;
+                       lines: while ((line = reader.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 = reader.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 UtilsException("Token size " + tokenSize
+                                                               + " is different from header size "
+                                                               + headerSize + " at line " + lineCount
+                                                               + ", line: " + line + ", header: " + header
+                                                               + ", tokens: " + tokens);
+                                       }
+                               }
+                               processLine(lineCount, header, tokens);
+                       }
+               } catch (UtilsException e) {
+                       throw e;
+               } catch (IOException e) {
+                       throw new UtilsException("Cannot parse CSV file (line: "
+                                       + lineCount + ")", e);
+               } finally {
+                       StreamUtils.closeQuietly(reader);
+               }
+       }
+
+       /**
+        * 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) {
+               // List<String> tokens = new ArrayList<String>();
+
+               // System.out.println("#LINE: " + str);
+
+               if (wasInquote)
+                       currStr.append('\n');
+
+               char[] arr = str.toCharArray();
+               boolean inQuote = wasInquote;
+               // StringBuffer currStr = new StringBuffer("");
+               for (int i = 0; i < arr.length; i++) {
+                       char c = arr[i];
+                       if (c == separator) {
+                               if (!inQuote) {
+                                       tokens.add(currStr.toString());
+                                       // System.out.println("# TOKEN: " + currStr);
+                                       currStr.delete(0, currStr.length());
+                               } 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
new file mode 100644 (file)
index 0000000..c84d994
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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 UtilsException("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
new file mode 100644 (file)
index 0000000..4b9fea3
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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.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.
+        */
+       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.
+        */
+       public CsvWriter(OutputStream out, String encoding) {
+               try {
+                       this.out = new OutputStreamWriter(out, encoding);
+               } catch (UnsupportedEncodingException e) {
+                       throw new UtilsException("Cannot initialize CSV writer", 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(List<?> tokens) {
+               try {
+                       Iterator<?> it = tokens.iterator();
+                       while (it.hasNext()) {
+                               writeToken(it.next().toString());
+                               if (it.hasNext())
+                                       out.write(separator);
+                       }
+                       out.write('\n');
+                       out.flush();
+               } catch (IOException e) {
+                       throw new UtilsException("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) {
+                                       // TODO configure how to deal with null
+                                       writeToken("");
+                               } else {
+                                       writeToken(tokens[i].toString());
+                               }
+                               if (i != (tokens.length - 1))
+                                       out.write(separator);
+                       }
+                       out.write('\n');
+                       out.flush();
+               } catch (IOException e) {
+                       throw new UtilsException("Could not write " + tokens, e);
+               }
+       }
+
+       protected void writeToken(String token) throws IOException {
+               // +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/DictionaryKeys.java b/org.argeo.util/src/org/argeo/util/DictionaryKeys.java
new file mode 100644 (file)
index 0000000..d17c86f
--- /dev/null
@@ -0,0 +1,42 @@
+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
new file mode 100644 (file)
index 0000000..068ff21
--- /dev/null
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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 SHA1 = "SHA1";
+
+       private static Boolean debug = false;
+       // TODO: make it writable
+       private final static Integer byteBufferCapacity = 100 * 1024;// 100 KB
+
+       public static byte[] sha1(byte[] bytes) {
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(SHA1);
+                       digest.update(bytes);
+                       byte[] checksum = digest.digest();
+                       return checksum;
+               } catch (Exception e) {
+                       throw new UtilsException("Cannot SHA1 digest", e);
+               }
+       }
+
+       public static String digest(String algorithm, byte[] bytes) {
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(algorithm);
+                       digest.update(bytes);
+                       byte[] checksum = digest.digest();
+                       String res = encodeHexString(checksum);
+                       return res;
+               } catch (Exception e) {
+                       throw new UtilsException("Cannot digest with algorithm " + algorithm, e);
+               }
+       }
+
+       public static String digest(String algorithm, InputStream in) {
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(algorithm);
+                       // ReadableByteChannel channel = Channels.newChannel(in);
+                       // ByteBuffer bb = ByteBuffer.allocateDirect(byteBufferCapacity);
+                       // while (channel.read(bb) > 0)
+                       // digest.update(bb);
+                       byte[] buffer = new byte[byteBufferCapacity];
+                       int read = 0;
+                       while ((read = in.read(buffer)) > 0) {
+                               digest.update(buffer, 0, read);
+                       }
+
+                       byte[] checksum = digest.digest();
+                       String res = encodeHexString(checksum);
+                       return res;
+               } catch (Exception e) {
+                       throw new UtilsException("Cannot digest with algorithm " + algorithm, 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 UtilsException("Cannot digest " + file + " with algorithm " + algorithm, e);
+               } finally {
+                       StreamUtils.closeQuietly(fis);
+                       if (fc.isOpen())
+                               try {
+                                       fc.close();
+                               } catch (IOException e) {
+                                       // silent
+                               }
+               }
+       }
+
+       protected static String digest(String algorithm, ByteBuffer bb) {
+               long begin = System.currentTimeMillis();
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(algorithm);
+                       digest.update(bb);
+                       byte[] checksum = digest.digest();
+                       String res = encodeHexString(checksum);
+                       long end = System.currentTimeMillis();
+                       if (debug)
+                               System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s");
+                       return res;
+               } catch (NoSuchAlgorithmException e) {
+                       throw new UtilsException("Cannot digest with algorithm " + algorithm, e);
+               }
+       }
+
+       public static String sha1hex(Path path) {
+               return digest(SHA1, path, byteBufferCapacity);
+       }
+
+       public static String digest(String algorithm, Path path, long bufferSize) {
+               byte[] digest = digestRaw(algorithm, path, bufferSize);
+               return encodeHexString(digest);
+       }
+
+       public static byte[] digestRaw(String algorithm, Path file, long bufferSize) {
+               long begin = System.currentTimeMillis();
+               try {
+                       MessageDigest md = MessageDigest.getInstance(algorithm);
+                       FileChannel fc = FileChannel.open(file);
+                       long fileSize = Files.size(file);
+                       if (fileSize <= bufferSize) {
+                               ByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, fileSize);
+                               md.update(bb);
+                       } else {
+                               long lastCycle = (fileSize / bufferSize) - 1;
+                               long position = 0;
+                               for (int i = 0; i <= lastCycle; i++) {
+                                       ByteBuffer bb;
+                                       if (i != lastCycle) {
+                                               bb = fc.map(MapMode.READ_ONLY, position, bufferSize);
+                                               position = position + bufferSize;
+                                       } else {
+                                               bb = fc.map(MapMode.READ_ONLY, position, fileSize - position);
+                                               position = fileSize;
+                                       }
+                                       md.update(bb);
+                               }
+                       }
+                       long end = System.currentTimeMillis();
+                       if (debug)
+                               System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s");
+                       return md.digest();
+               } catch (Exception e) {
+                       throw new UtilsException("Cannot digest " + file + "  with algorithm " + algorithm, e);
+               }
+       }
+
+       public static void main(String[] args) {
+               File file;
+               if (args.length > 0)
+                       file = new File(args[0]);
+               else {
+                       System.err.println("Usage: <file> [<algorithm>]" + " (see http://java.sun.com/j2se/1.5.0/"
+                                       + "docs/guide/security/CryptoSpec.html#AppA)");
+                       return;
+               }
+
+               if (args.length > 1) {
+                       String algorithm = args[1];
+                       System.out.println(digest(algorithm, file));
+               } else {
+                       String algorithm = "MD5";
+                       System.out.println(algorithm + ": " + digest(algorithm, file));
+                       algorithm = "SHA";
+                       System.out.println(algorithm + ": " + digest(algorithm, file));
+                       System.out.println(algorithm + ": " + sha1hex(file.toPath()));
+                       algorithm = "SHA-256";
+                       System.out.println(algorithm + ": " + digest(algorithm, file));
+                       algorithm = "SHA-512";
+                       System.out.println(algorithm + ": " + digest(algorithm, file));
+               }
+       }
+
+       final private static char[] hexArray = "0123456789abcdef".toCharArray();
+
+       /**
+        * From
+        * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
+        * -a-hex-string-in-java
+        */
+       public static String encodeHexString(byte[] bytes) {
+               char[] hexChars = new char[bytes.length * 2];
+               for (int j = 0; j < bytes.length; j++) {
+                       int v = bytes[j] & 0xFF;
+                       hexChars[j * 2] = hexArray[v >>> 4];
+                       hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+               }
+               return new String(hexChars);
+       }
+
+}
diff --git a/org.argeo.util/src/org/argeo/util/DirH.java b/org.argeo.util/src/org/argeo/util/DirH.java
new file mode 100644 (file)
index 0000000..4035b96
--- /dev/null
@@ -0,0 +1,116 @@
+package org.argeo.util;
+
+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 UtilsException(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 UtilsException(
+                                               "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 UtilsException("Cannot digest", e);
+               }
+       }
+
+       public void print(PrintStream out) {
+               out.print(DigestUtils.encodeHexString(digest));
+               if (dirName.length > 0) {
+                       out.print(' ');
+                       out.print(new String(dirName, charset));
+               }
+               out.print('\n');
+               for (int i = 0; i < hashes.length; i++) {
+                       out.print(DigestUtils.encodeHexString(hashes[i]));
+                       out.print(' ');
+                       out.print(new String(fileNames[i], charset));
+                       out.print('\n');
+               }
+       }
+
+       public static DirH digest(Path dir) {
+               try (DirectoryStream<Path> files = Files.newDirectoryStream(dir)) {
+                       List<byte[]> hs = new ArrayList<byte[]>();
+                       List<String> fNames = new ArrayList<>();
+                       for (Path file : files) {
+                               if (!Files.isDirectory(file)) {
+                                       byte[] digest = DigestUtils.digestRaw(algorithm, file, bufferSize);
+                                       hs.add(digest);
+                                       fNames.add(file.getFileName().toString());
+                               }
+                       }
+
+                       byte[][] fileNames = new byte[fNames.size()][];
+                       for (int i = 0; i < fNames.size(); i++) {
+                               fileNames[i] = fNames.get(i).getBytes(charset);
+                       }
+                       byte[][] hashes = hs.toArray(new byte[hs.size()][]);
+                       return new DirH(hashes, fileNames, dir.toString().getBytes(charset));
+               } catch (Exception e) {
+                       throw new UtilsException("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/LangUtils.java b/org.argeo.util/src/org/argeo/util/LangUtils.java
new file mode 100644 (file)
index 0000000..b791f49
--- /dev/null
@@ -0,0 +1,192 @@
+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.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Properties;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+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;
+       }
+
+       /*
+        * DICTIONARY
+        */
+
+       /**
+        * Creates a new {@link Dictionary} with one key-value pair (neither key not
+        * value should be null)
+        */
+       public static Dictionary<String, Object> dico(String key, Object value) {
+               assert key != null;
+               assert value != null;
+               Hashtable<String, Object> props = new Hashtable<>();
+               props.put(key, value);
+               return props;
+       }
+
+       /**
+        * 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;
+       }
+
+       /*
+        * 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/PasswordEncryption.java b/org.argeo.util/src/org/argeo/util/PasswordEncryption.java
new file mode 100644 (file)
index 0000000..7269689
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+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.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";
+
+       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 (Exception e1) {
+                               throw new UtilsException("Cannot get secret key (with restricted length)", e1);
+                       }
+               } catch (Exception e) {
+                       throw new UtilsException("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;
+               } catch (Exception e) {
+                       throw new UtilsException("Cannot encrypt", 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;
+               } catch (Exception e) {
+                       throw new UtilsException("Cannot decrypt", 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 (Exception e) {
+                       throw new UtilsException("Cannot encrypt", 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 (Exception e) {
+                       throw new UtilsException("Cannot decrypt", 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/StreamUtils.java b/org.argeo.util/src/org/argeo/util/StreamUtils.java
new file mode 100644 (file)
index 0000000..1cfa5b2
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+
+/** Utilities to be used when APache COmmons IO is not available. */
+class StreamUtils {
+       private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
+
+       /*
+        * APACHE COMMONS IO (inspired)
+        */
+
+       /** @return the number of bytes */
+       public static Long copy(InputStream in, OutputStream out)
+                       throws IOException {
+               Long count = 0l;
+               byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
+               while (true) {
+                       int length = in.read(buf);
+                       if (length < 0)
+                               break;
+                       out.write(buf, 0, length);
+                       count = count + length;
+               }
+               return count;
+       }
+
+       /** @return the number of chars */
+       public static Long copy(Reader in, Writer out) throws IOException {
+               Long count = 0l;
+               char[] buf = new char[DEFAULT_BUFFER_SIZE];
+               while (true) {
+                       int length = in.read(buf);
+                       if (length < 0)
+                               break;
+                       out.write(buf, 0, length);
+                       count = count + length;
+               }
+               return count;
+       }
+
+       public static void closeQuietly(InputStream in) {
+               if (in != null)
+                       try {
+                               in.close();
+                       } catch (Exception e) {
+                               //
+                       }
+       }
+
+       public static void closeQuietly(OutputStream out) {
+               if (out != null)
+                       try {
+                               out.close();
+                       } catch (Exception e) {
+                               //
+                       }
+       }
+
+       public static void closeQuietly(Reader in) {
+               if (in != null)
+                       try {
+                               in.close();
+                       } catch (Exception e) {
+                               //
+                       }
+       }
+
+       public static void closeQuietly(Writer out) {
+               if (out != null)
+                       try {
+                               out.close();
+                       } catch (Exception e) {
+                               //
+                       }
+       }
+}
diff --git a/org.argeo.util/src/org/argeo/util/Throughput.java b/org.argeo.util/src/org/argeo/util/Throughput.java
new file mode 100644 (file)
index 0000000..e5bc961
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.util;
+
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.util.Locale;
+
+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 UtilsException("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 UtilsException(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 UtilsException("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 UtilsException("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/UtilsException.java b/org.argeo.util/src/org/argeo/util/UtilsException.java
new file mode 100644 (file)
index 0000000..b93233a
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.util;
+
+/** Utils specific exception. */
+class UtilsException extends RuntimeException {
+       private static final long serialVersionUID = 1L;
+
+       /** Creates an exception with a message. */
+       public UtilsException(String message) {
+               super(message);
+       }
+
+       /** Creates an exception with a message and a root cause. */
+       public UtilsException(String message, Throwable e) {
+               super(message, e);
+       }
+
+}
diff --git a/pom.xml b/pom.xml
new file mode 100644 (file)
index 0000000..02c0281
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,548 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <groupId>org.argeo.commons</groupId>
+       <artifactId>argeo-commons</artifactId>
+       <version>2.1.76-SNAPSHOT</version>
+       <name>Argeo Commons</name>
+       <packaging>pom</packaging>
+       <!-- <url>http://repo.argeo.org/data/docs/argeo-2.1/site/argeo-commons/</url> -->
+       <properties>
+               <version.argeo-commons>2.1.76-SNAPSHOT</version.argeo-commons>
+               <version.argeo-tp>2.1.21</version.argeo-tp>
+               <!-- RPM -->
+               <argeo.rpm.release>7</argeo.rpm.release>
+               <argeo.rpm.stagingRepository>/srv/rpmfactory/argeo-osgi-2/el7</argeo.rpm.stagingRepository>
+               <!-- Encoding, see http://is.gd/mvn_source_encoding -->
+               <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+               <project.scm.id>code.argeo.org</project.scm.id>
+       </properties>
+       <modules>
+               <!-- Base -->
+               <module>org.argeo.util</module>
+               <module>org.argeo.enterprise</module>
+               <module>org.argeo.jcr</module>
+               <module>org.argeo.osgi.boot</module>
+               <!-- Eclipse -->
+               <module>org.argeo.eclipse.ui</module>
+               <module>org.argeo.eclipse.ui.rap</module>
+               <!-- CMS -->
+               <module>org.argeo.node.api</module>
+               <module>org.argeo.maintenance</module>
+               <module>org.argeo.cms</module>
+               <module>org.argeo.cms.ui.theme</module>
+               <module>org.argeo.cms.ui</module>
+               <!-- CMS E4 -->
+               <module>org.argeo.cms.e4</module>
+               <module>org.argeo.cms.e4.rap</module>
+               <!-- CMS Workbench -->
+               <module>org.argeo.cms.ui.workbench</module>
+               <module>org.argeo.cms.ui.workbench.rap</module>
+               <!-- Third Parties Extensions -->
+               <module>org.argeo.ext.jackrabbit</module>
+               <module>org.argeo.ext.rap.ui.workbench</module>
+               <!-- Distribution -->
+               <module>maven</module>
+               <module>dep</module>
+               <module>demo</module>
+               <module>doc</module>
+               <module>dist</module>
+               <module>rcp</module>
+       </modules>
+       <scm>
+               <connection>scm:git:http://git.argeo.org/apache2/argeo-commons.git</connection>
+               <url>http://git.argeo.org/?p=apache2/argeo-commons.git;a=summary</url>
+               <developerConnection>scm:git:https://code.argeo.org/git/apache2/argeo-commons.git</developerConnection>
+               <tag>HEAD</tag>
+       </scm>
+       <inceptionYear>2007</inceptionYear>
+       <licenses>
+               <license>
+                       <name>Apache 2</name>
+                       <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+                       <distribution>repo</distribution>
+                       <comments><![CDATA[
+Argeo Commons Enterprise Framework
+                          
+Copyright (C) 2007-2016 Argeo GmbH
+
+Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an &quot;AS IS&quot; BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+]]>
+                       </comments>
+               </license>
+       </licenses>
+       <build>
+               <extensions>
+                       <extension>
+                               <groupId>org.apache.maven.wagon</groupId>
+                               <artifactId>wagon-webdav-jackrabbit</artifactId>
+                               <version>2.10</version>
+                       </extension>
+               </extensions>
+               <sourceDirectory>src</sourceDirectory>
+               <testSourceDirectory>ext/test</testSourceDirectory>
+               <resources>
+                       <resource>
+                               <directory>src</directory>
+                               <includes>
+                                       <include>**</include>
+                               </includes>
+                               <excludes>
+                                       <exclude>**/*.java</exclude>
+                               </excludes>
+                       </resource>
+                       <resource>
+                               <directory>.</directory>
+                               <includes>
+                                       <include>**</include>
+                               </includes>
+                               <excludes>
+                                       <exclude>.*</exclude>
+                                       <exclude>.*/**</exclude>
+                                       <exclude>src/**</exclude>
+                                       <exclude>ext/**</exclude>
+                                       <exclude>target/**</exclude>
+                                       <exclude>bin/**</exclude>
+                                       <exclude>pom.xml</exclude>
+                                       <exclude>build.properties</exclude>
+                                       <exclude>*.bnd</exclude>
+                                       <exclude>*.target</exclude>
+                                       <exclude>**/*.xcf</exclude>
+                               </excludes>
+                       </resource>
+               </resources>
+               <testResources>
+                       <testResource>
+                               <directory>ext/test</directory>
+                               <includes>
+                                       <include>**</include>
+                               </includes>
+                               <excludes>
+                                       <exclude>**/*.java</exclude>
+                               </excludes>
+                       </testResource>
+               </testResources>
+               <pluginManagement>
+                       <plugins>
+                               <!-- Maven -->
+                               <plugin>
+                                       <artifactId>maven-compiler-plugin</artifactId>
+                                       <version>3.3</version>
+                                       <configuration>
+                                               <source>1.8</source>
+                                               <target>1.8</target>
+                                               <compilerId>eclipse</compilerId>
+                                               <!-- Required for compliance level, see http://jira.codehaus.org/browse/PLXCOMP-231 -->
+                                               <optimize>true</optimize>
+                                               <!-- Hack to work around issues with generated annotations : -->
+                                               <generatedSourcesDirectory>target/classes</generatedSourcesDirectory>
+                                               <generatedTestSourcesDirectory>target/test-classes</generatedTestSourcesDirectory>
+                                       </configuration>
+                                       <dependencies>
+                                               <dependency>
+                                                       <groupId>org.codehaus.plexus</groupId>
+                                                       <artifactId>plexus-compiler-eclipse</artifactId>
+                                                       <version>2.6</version>
+                                               </dependency>
+                                       </dependencies>
+                               </plugin>
+                               <plugin>
+                                       <artifactId>maven-source-plugin</artifactId>
+                                       <version>2.4</version>
+                                       <executions>
+                                               <execution>
+                                                       <id>attach-sources</id>
+                                                       <phase>package</phase>
+                                                       <goals>
+                                                               <goal>jar</goal>
+                                                       </goals>
+                                               </execution>
+                                       </executions>
+                                       <configuration>
+                                               <excludes>
+                                                       <!-- Prevents source jars to contain misleading data -->
+                                                       <exclude>plugin.xml</exclude>
+                                                       <exclude>META-INF/MANIFEST.MF</exclude>
+                                               </excludes>
+                                       </configuration>
+                               </plugin>
+                               <plugin>
+                                       <artifactId>maven-clean-plugin</artifactId>
+                                       <version>2.6.1</version>
+                                       <configuration>
+                                               <filesets>
+                                                       <fileset>
+                                                               <directory>META-INF</directory>
+                                                               <includes>
+                                                                       <include>MANIFEST.MF</include>
+                                                               </includes>
+                                                       </fileset>
+                                               </filesets>
+                                       </configuration>
+                               </plugin>
+                               <plugin>
+                                       <artifactId>maven-surefire-plugin</artifactId>
+                                       <version>2.18</version>
+                               </plugin>
+                               <plugin>
+                                       <artifactId>maven-jar-plugin</artifactId>
+                                       <version>2.5</version>
+                                       <configuration>
+                                               <archive>
+                                                       <manifestFile>META-INF/MANIFEST.MF</manifestFile>
+                                               </archive>
+                                       </configuration>
+                               </plugin>
+                               <plugin>
+                                       <artifactId>maven-antrun-plugin</artifactId>
+                                       <version>1.8</version>
+                               </plugin>
+                               <plugin>
+                                       <artifactId>maven-resources-plugin</artifactId>
+                                       <version>2.7</version>
+                               </plugin>
+                               <plugin>
+                                       <artifactId>maven-dependency-plugin</artifactId>
+                                       <version>2.9</version>
+                               </plugin>
+                               <plugin>
+                                       <artifactId>maven-site-plugin</artifactId>
+                                       <version>3.7</version>
+                               </plugin>
+                               <plugin>
+                                       <artifactId>maven-javadoc-plugin</artifactId>
+                                       <version>3.0.0</version>
+                                       <configuration>
+                                               <failOnError>false</failOnError>
+                                               <additionalJOption>-Xdoclint:none</additionalJOption>
+                                               <excludePackageNames>*.internal.*,org.eclipse.*</excludePackageNames>
+                                               <encoding>UTF-8</encoding>
+                                       </configuration>
+                               </plugin>
+                               <plugin>
+                                       <artifactId>maven-release-plugin</artifactId>
+                                       <version>2.5.1</version>
+                                       <configuration>
+                                               <autoVersionSubmodules>true</autoVersionSubmodules>
+                                       </configuration>
+                               </plugin>
+                               <plugin>
+                                       <groupId>org.apache.felix</groupId>
+                                       <artifactId>maven-bundle-plugin</artifactId>
+                                       <version>3.0.1</version>
+                                       <extensions>true</extensions>
+                                       <configuration>
+                                               <manifestLocation>META-INF</manifestLocation>
+                                               <instructions>
+                                                       <_include>bnd.bnd</_include>
+                                                       <Bundle-Version>${project.version}-r${tstamp}</Bundle-Version>
+                                                       <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
+                                                       <Bundle-RequiredExecutionEnvironment>JavaSE-1.8</Bundle-RequiredExecutionEnvironment>
+                                                       <_removeheaders>Bnd-LastModified,Build-Jdk,Built-By,Tool,Created-By</_removeheaders>
+                                                       <Automatic-Module-Name>${project.artifactId}</Automatic-Module-Name>
+                                                       <SLC-Category>${project.groupId}</SLC-Category>
+                                               </instructions>
+                                       </configuration>
+                                       <executions>
+                                               <execution>
+                                                       <id>bundle-manifest</id>
+                                                       <phase>process-classes</phase>
+                                                       <goals>
+                                                               <goal>manifest</goal>
+                                                       </goals>
+                                               </execution>
+                                       </executions>
+                               </plugin>
+                               <!-- Codehaus -->
+                               <plugin>
+                                       <groupId>org.codehaus.mojo</groupId>
+                                       <artifactId>rpm-maven-plugin</artifactId>
+                                       <version>2.1.4</version>
+                                       <extensions>true</extensions>
+                                       <configuration>
+                                               <version>${project.version}</version>
+                                               <distribution>argeo${argeo.rpm.release}</distribution>
+                                               <group>Applications/System</group>
+                                               <prefix>/usr</prefix>
+                                               <defaultDirMode>755</defaultDirMode>
+                                               <defaultFileMode>644</defaultFileMode>
+                                               <autoRequires>false</autoRequires>
+                                       </configuration>
+                               </plugin>
+                               <plugin>
+                                       <groupId>org.codehaus.mojo</groupId>
+                                       <artifactId>exec-maven-plugin</artifactId>
+                                       <version>1.3.2</version>
+                               </plugin>
+                               <!-- Argeo -->
+                               <plugin>
+                                       <groupId>org.argeo.maven.plugins</groupId>
+                                       <artifactId>maven-argeo-osgi-plugin</artifactId>
+                                       <version>1.1.6</version>
+                               </plugin>
+                       </plugins>
+               </pluginManagement>
+               <plugins>
+                       <plugin>
+                               <artifactId>maven-clean-plugin</artifactId>
+                               <configuration>
+                                       <filesets>
+                                               <fileset>
+                                                       <directory>META-INF</directory>
+                                                       <includes>
+                                                               <include>MANIFEST.MF</include>
+                                                       </includes>
+                                               </fileset>
+                                       </filesets>
+                               </configuration>
+                       </plugin>
+                       <plugin>
+                               <artifactId>maven-resources-plugin</artifactId>
+                               <configuration>
+                                       <encoding>UTF-8</encoding>
+                               </configuration>
+                       </plugin>
+                       <plugin>
+                               <artifactId>maven-site-plugin</artifactId>
+                               <inherited>false</inherited>
+                               <configuration>
+                                       <skip>false</skip>
+                               </configuration>
+                       </plugin>
+                       <plugin>
+                               <artifactId>maven-release-plugin</artifactId>
+                               <configuration>
+                                       <goals>deploy</goals>
+                                       <releaseProfiles>rpmbuild,rpmbuild-tp</releaseProfiles>
+                               </configuration>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.apache.felix</groupId>
+                               <artifactId>maven-bundle-plugin</artifactId>
+                       </plugin>
+               </plugins>
+       </build>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.tp</groupId>
+                       <artifactId>argeo-tp</artifactId>
+                       <version>${version.argeo-tp}</version>
+                       <scope>provided</scope>
+                       <exclusions>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.apache</groupId>
+                                       <artifactId>org.apache.xerces</artifactId>
+                               </exclusion>
+                       </exclusions>
+               </dependency>
+       </dependencies>
+       <dependencyManagement>
+               <dependencies>
+                       <dependency>
+                               <groupId>org.argeo.tp</groupId>
+                               <artifactId>argeo-tp</artifactId>
+                               <version>${version.argeo-tp}</version>
+                               <type>pom</type>
+                               <scope>import</scope>
+                       </dependency>
+                       <dependency>
+                               <groupId>org.argeo.tp</groupId>
+                               <artifactId>argeo-tp-rap-e4</artifactId>
+                               <version>${version.argeo-tp}</version>
+                               <type>pom</type>
+                               <scope>import</scope>
+                       </dependency>
+               </dependencies>
+       </dependencyManagement>
+       <repositories>
+               <repository>
+                       <id>argeo</id>
+                       <url>http://repo.argeo.org/data/java/argeo-2.1/</url>
+                       <releases>
+                               <enabled>true</enabled>
+                               <updatePolicy>daily</updatePolicy>
+                               <checksumPolicy>warn</checksumPolicy>
+                       </releases>
+               </repository>
+       </repositories>
+       <pluginRepositories>
+               <pluginRepository>
+                       <id>argeo-maven-plugins</id>
+                       <url>http://repo.argeo.org/data/java/argeo-2.1</url>
+                       <releases>
+                               <enabled>true</enabled>
+                               <updatePolicy>daily</updatePolicy>
+                               <checksumPolicy>warn</checksumPolicy>
+                       </releases>
+               </pluginRepository>
+       </pluginRepositories>
+       <reporting>
+               <plugins>
+                       <plugin>
+                               <artifactId>maven-project-info-reports-plugin</artifactId>
+                               <version>2.9</version>
+                               <reportSets>
+                                       <reportSet>
+                                               <reports>
+                                                       <report>index</report>
+                                                       <report>summary</report>
+                                                       <report>license</report>
+                                                       <report>scm</report>
+                                               </reports>
+                                       </reportSet>
+                               </reportSets>
+                       </plugin>
+                       <plugin>
+                               <artifactId>maven-javadoc-plugin</artifactId>
+                               <version>3.0.0</version>
+                               <configuration>
+                                       <failOnError>false</failOnError>
+                                       <additionalJOption>-Xdoclint:none</additionalJOption>
+                                       <excludePackageNames>*.internal.*,org.eclipse.*</excludePackageNames>
+                                       <encoding>UTF-8</encoding>
+                                       <detectLinks>true</detectLinks>
+                                       <links>
+                                               <link>http://docs.oracle.com/javase/8/docs/api</link>
+                                               <link>https://osgi.org/javadoc/r5/core</link>
+                                               <link>https://osgi.org/javadoc/r5/enterprise</link>
+                                               <link>https://docs.adobe.com/docs/en/spec/javax.jcr/javadocs/jcr-2.0</link>
+                                               <link>http://help.eclipse.org/oxygen/topic/org.eclipse.platform.doc.isv/reference/api</link>
+                                               <link>http://docs.spring.io/spring/docs/3.2.x/javadoc-api</link>
+                                       </links>
+                               </configuration>
+                               <reportSets>
+                                       <reportSet>
+                                               <id>aggregate-javadoc</id>
+                                               <inherited>false</inherited>
+                                               <reports>
+                                                       <report>aggregate</report>
+                                               </reports>
+                                       </reportSet>
+                                       <reportSet>
+                                               <id>javadoc</id>
+                                               <reports />
+                                       </reportSet>
+                               </reportSets>
+                       </plugin>
+                       <plugin>
+                               <artifactId>maven-jxr-plugin</artifactId>
+                               <version>2.5</version>
+                               <reportSets>
+                                       <reportSet>
+                                               <id>aggregate-jxr</id>
+                                               <inherited>false</inherited>
+                                               <reports>
+                                                       <report>aggregate</report>
+                                               </reports>
+                                       </reportSet>
+                                       <reportSet>
+                                               <id>jxr</id>
+                                               <reports />
+                                       </reportSet>
+                               </reportSets>
+                       </plugin>
+               </plugins>
+       </reporting>
+       <distributionManagement>
+               <repository>
+                       <id>staging</id>
+                       <url>dav:https://forge.argeo.org/data/java/argeo-2.1/</url>
+                       <uniqueVersion>false</uniqueVersion>
+               </repository>
+               <site>
+                       <id>staging</id>
+                       <url>file:///srv/docfactory/argeo-2.1/site/argeo-commons/</url>
+               </site>
+       </distributionManagement>
+       <profiles>
+               <profile>
+                       <id>rpmbuild</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-antrun-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <phase>install</phase>
+                                                               <goals>
+                                                                       <goal>run</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <target>
+                                                                               <copy todir="${argeo.rpm.stagingRepository}" quiet="true"
+                                                                                       failonerror="false">
+                                                                                       <fileset dir="${project.build.directory}/rpm"
+                                                                                               includes="*/RPMS/**/*.rpm" />
+                                                                                       <flattenmapper />
+                                                                               </copy>
+                                                                       </target>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+               <profile>
+                       <id>rpmbuild-tp</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-antrun-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <phase>install</phase>
+                                                               <goals>
+                                                                       <goal>run</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <target>
+                                                                               <copy todir="${argeo.rpm.stagingRepository}" quiet="true"
+                                                                                       failonerror="false">
+                                                                                       <fileset dir="${project.build.directory}/rpm"
+                                                                                               includes="*/RPMS/**/*.rpm" />
+                                                                                       <flattenmapper />
+                                                                               </copy>
+                                                                       </target>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+               <profile>
+                       <id>localrepo</id>
+                       <repositories>
+                               <repository>
+                                       <id>argeo</id>
+                                       <url>http://localhost:7080/data/java/argeo-2.1</url>
+                                       <releases>
+                                               <enabled>true</enabled>
+                                               <updatePolicy>daily</updatePolicy>
+                                               <checksumPolicy>warn</checksumPolicy>
+                                       </releases>
+                               </repository>
+                       </repositories>
+                       <distributionManagement>
+                               <repository>
+                                       <id>staging</id>
+                                       <url>dav:http://localhost:7080/data/java/argeo-2.1/</url>
+                                       <uniqueVersion>false</uniqueVersion>
+                               </repository>
+                       </distributionManagement>
+               </profile>
+       </profiles>
+</project>
diff --git a/rcp/.gitignore b/rcp/.gitignore
new file mode 100644 (file)
index 0000000..b83d222
--- /dev/null
@@ -0,0 +1 @@
+/target/
diff --git a/rcp/demo/.gitignore b/rcp/demo/.gitignore
new file mode 100644 (file)
index 0000000..45dfa56
--- /dev/null
@@ -0,0 +1 @@
+/exec/
diff --git a/rcp/demo/argeo-companion.properties b/rcp/demo/argeo-companion.properties
new file mode 100644 (file)
index 0000000..95dcd98
--- /dev/null
@@ -0,0 +1,18 @@
+argeo.osgi.start.2.node=\
+org.eclipse.equinox.metatype,\
+org.eclipse.equinox.cm,\
+
+argeo.osgi.start.3.node=\
+org.argeo.cms
+
+applicationXMI=org.argeo.cms.e4.rcp/argeo-companion.e4xmi
+lifeCycleURI=bundleclass://org.argeo.cms.e4.rcp/org.argeo.cms.e4.rcp.CmsRcpLifeCycle
+clearPersistedState=true
+#argeo.cms.desktop.inTray=true
+
+# Remote node:
+#argeo.node.repo.labeledUri=http://root:demo@localhost:7070/jcr/node
+
+log4j.configuration=file:../../log4j.properties
+argeo.node.useradmin.uris=os:///
+eclipse.application=org.argeo.cms.e4.rcp.CmsE4Application
diff --git a/rcp/demo/log4j.properties b/rcp/demo/log4j.properties
new file mode 100644 (file)
index 0000000..13f949f
--- /dev/null
@@ -0,0 +1,32 @@
+log4j.rootLogger=WARN, development
+
+## Levels
+log4j.logger.org.argeo=DEBUG
+log4j.logger.org.argeo.jackrabbit.remote.ExtendedDispatcherServlet=WARN
+log4j.logger.org.argeo.server.webextender.TomcatDeployer=INFO
+
+#log4j.logger.org.springframework.security=DEBUG
+#log4j.logger.org.apache.commons.exec=DEBUG
+#log4j.logger.org.apache.jackrabbit.webdav=DEBUG
+#log4j.logger.org.apache.jackrabbit.remote=DEBUG
+#log4j.logger.org.apache.jackrabbit.core.observation=DEBUG
+
+log4j.logger.org.apache.catalina=INFO
+log4j.logger.org.apache.coyote=INFO
+
+log4j.logger.org.apache.directory=INFO
+log4j.logger.org.apache.directory.server=ERROR
+log4j.logger.org.apache.jackrabbit.core.query.lucene=ERROR
+
+## Appenders
+# console is set to be a ConsoleAppender.
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+
+# console uses PatternLayout.
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern= %-5p %d{ISO8601} %m - %c - [%t]%n
+
+# development appender (slow!)
+log4j.appender.development=org.apache.log4j.ConsoleAppender
+log4j.appender.development.layout=org.apache.log4j.PatternLayout
+log4j.appender.development.layout.ConversionPattern=%d{HH:mm:ss} [%16.16t] %5p %m (%F:%L) %c%n
diff --git a/rcp/dep/.gitignore b/rcp/dep/.gitignore
new file mode 100644 (file)
index 0000000..b83d222
--- /dev/null
@@ -0,0 +1 @@
+/target/
diff --git a/rcp/dep/org.argeo.dep.cms.e4.rcp/.gitignore b/rcp/dep/org.argeo.dep.cms.e4.rcp/.gitignore
new file mode 100644 (file)
index 0000000..5e85f8d
--- /dev/null
@@ -0,0 +1,3 @@
+/org.argeo.security.dep.node.rcp-maven.target
+/target/
+/*.target
diff --git a/rcp/dep/org.argeo.dep.cms.e4.rcp/META-INF/.gitignore b/rcp/dep/org.argeo.dep.cms.e4.rcp/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/rcp/dep/org.argeo.dep.cms.e4.rcp/bnd.bnd b/rcp/dep/org.argeo.dep.cms.e4.rcp/bnd.bnd
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/rcp/dep/org.argeo.dep.cms.e4.rcp/p2.inf b/rcp/dep/org.argeo.dep.cms.e4.rcp/p2.inf
new file mode 100644 (file)
index 0000000..0423aa5
--- /dev/null
@@ -0,0 +1,2 @@
+properties.1.name=org.eclipse.equinox.p2.type.category
+properties.1.value=true
\ No newline at end of file
diff --git a/rcp/dep/org.argeo.dep.cms.e4.rcp/pom.xml b/rcp/dep/org.argeo.dep.cms.e4.rcp/pom.xml
new file mode 100644 (file)
index 0000000..6e6e067
--- /dev/null
@@ -0,0 +1,808 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons.rcp</groupId>
+               <artifactId>dep</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.dep.cms.e4.rcp</artifactId>
+       <name>Node Eclipse RCP</name>
+       <dependencies>
+               <!-- SWT for ARM -->
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.swt.gtk.linux.arm</artifactId>
+                       <version>3.108.0.v20180905-1254</version>
+               </dependency>
+
+               <!-- Eclipse launcher -->
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.equinox.launcher</artifactId>
+                       <version>1.5.100.v20180827-1352</version>
+               </dependency>
+
+               <!-- RCP -->
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.css.swt.theme</artifactId>
+                       <version>0.12.100.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.workbench.swt</artifactId>
+                       <version>0.14.300.v20180906-1121</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.core.contenttype</artifactId>
+                       <version>3.7.100.v20180817-1401</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.batik.constants</artifactId>
+                       <version>1.10.0.v20180703-1553</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di</artifactId>
+                       <version>1.7.100.v20180817-1215</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.swt.gtk.linux.x86</artifactId>
+                       <version>3.108.0.v20180904-1901</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.core.runtime</artifactId>
+                       <version>3.15.0.v20180817-1401</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.core.databinding</artifactId>
+                       <version>1.7.0.v20180827-2028</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.bindings</artifactId>
+                       <version>0.12.200.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.model.workbench</artifactId>
+                       <version>2.1.100.v20180904-1914</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.ui.cocoa</artifactId>
+                       <version>1.2.100.v20180828-0838</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.felix.scr</artifactId>
+                       <version>2.0.14.v20180822-1822</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.w3c.dom.svg</artifactId>
+                       <version>1.1.0.v201011041433</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.jface.databinding</artifactId>
+                       <version>1.8.300.v20180828-0836</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.workbench.addons.swt</artifactId>
+                       <version>1.3.200.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.services</artifactId>
+                       <version>2.1.200.v20180817-1215</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.felix.gogo.shell</artifactId>
+                       <version>1.1.0.v20180713-1646</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.widgets</artifactId>
+                       <version>1.2.200.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.commands</artifactId>
+                       <version>0.12.300.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di.extensions.supplier</artifactId>
+                       <version>0.15.200.v20180817-1215</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.core.expressions</artifactId>
+                       <version>3.6.200.v20180817-1401</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.equinox.launcher.gtk.linux.x86_64</artifactId>
+                       <version>1.1.800.v20180827-1352</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>com.ibm.icu</artifactId>
+                       <version>62.1.0.v20180727-1652</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.batik.css</artifactId>
+                       <version>1.10.0.v20180703-1553</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.css.swt</artifactId>
+                       <version>0.13.200.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.swt.gtk</artifactId>
+                       <version>1.0.400.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.batik.i18n</artifactId>
+                       <version>1.10.0.v20180703-1553</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.w3c.css.sac</artifactId>
+                       <version>1.3.1.v200903091627</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.equinox.launcher.gtk.linux.x86</artifactId>
+                       <version>1.1.800.v20180827-1352</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.jface</artifactId>
+                       <version>3.14.100.v20180828-0836</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.emf.xpath</artifactId>
+                       <version>0.2.200.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.swt.cocoa.macosx.x86_64</artifactId>
+                       <version>3.108.0.v20180904-1901</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.equinox.launcher.win32.win32.x86</artifactId>
+                       <version>1.1.800.v20180827-1352</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.equinox.launcher.cocoa.macosx.x86_64</artifactId>
+                       <version>1.1.800.v20180827-1352</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.felix.gogo.runtime</artifactId>
+                       <version>1.1.0.v20180713-1646</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.core.databinding.observable</artifactId>
+                       <version>1.6.300.v20180827-2028</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.rcp</artifactId>
+                       <version>4.9.100.v20180906-0745</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.equinox.launcher.win32.win32.x86_64</artifactId>
+                       <version>1.1.800.v20180827-1352</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.help</artifactId>
+                       <version>3.8.200.v20180821-0700</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.workbench.renderers.swt</artifactId>
+                       <version>0.14.300.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.w3c.dom.smil</artifactId>
+                       <version>1.0.1.v200903091627</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.swt.win32.win32.x86</artifactId>
+                       <version>3.108.0.v20180904-1901</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.core.databinding.property</artifactId>
+                       <version>1.6.300.v20180827-2028</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.commons.io</artifactId>
+                       <version>2.2.0.v201405211200</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.w3c.dom.events</artifactId>
+                       <version>3.0.0.draft20060413_v201105210656</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.di</artifactId>
+                       <version>1.2.300.v20180906-1121</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.dialogs</artifactId>
+                       <version>1.1.300.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di.extensions</artifactId>
+                       <version>0.15.200.v20180817-1215</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.equinox.bidi</artifactId>
+                       <version>1.1.200.v20180827-1235</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di.annotations</artifactId>
+                       <version>1.6.200.v20180817-1215</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.commons.logging</artifactId>
+                       <version>1.2.0.v20180409-1502</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.felix.gogo.command</artifactId>
+                       <version>1.0.2.v20170914-1324</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.workbench3</artifactId>
+                       <version>0.14.200.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.core.jobs</artifactId>
+                       <version>3.10.100.v20180817-1215</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.commons.jxpath</artifactId>
+                       <version>1.3.0.v200911051830</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.contexts</artifactId>
+                       <version>1.7.100.v20180817-1215</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.swt.gtk.linux.x86_64</artifactId>
+                       <version>3.108.0.v20180904-1901</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.core.databinding.beans</artifactId>
+                       <version>1.4.200.v20180827-2028</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.css.core</artifactId>
+                       <version>0.12.300.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.ui.workbench</artifactId>
+                       <version>3.112.0.v20180906-1121</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.swt.win32.win32.x86_64</artifactId>
+                       <version>3.108.0.v20180904-1901</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.batik.util</artifactId>
+                       <version>1.10.0.v20180703-1553</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.core.commands</artifactId>
+                       <version>3.9.200.v20180827-1727</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.services</artifactId>
+                       <version>1.3.200.v20180906-1121</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.ui</artifactId>
+                       <version>3.110.0.v20180828-1350</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.apache.xmlgraphics</artifactId>
+                       <version>2.2.0.v20180809-1640</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.workbench.renderers.swt.cocoa</artifactId>
+                       <version>0.12.100.v20180828-0227</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.swt.gtk.linux.ppc64le</artifactId>
+                       <version>3.108.0.v20180904-1901</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.workbench</artifactId>
+                       <version>1.7.0.v20180906-1121</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rcp.e4</groupId>
+                       <artifactId>org.eclipse.swt</artifactId>
+                       <version>3.108.0.v20180904-1901</version>
+               </dependency>
+
+               <!-- RCP -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp</groupId> -->
+               <!-- <artifactId>argeo-tp-rcp-e4</artifactId> -->
+               <!-- <version>${version.argeo-tp}</version> -->
+               <!-- <exclusions> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.osgi</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.osgi.services</artifactId> -->
+               <!-- </exclusion> -->
+
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.osgi.compatibility.state</artifactId> -->
+               <!-- </exclusion> -->
+
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.update.configurator</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.carbon.macosx</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.gtk.solaris.sparc</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.gtk.solaris.x86</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.motif.solaris.sparc</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.gtk.linux.s390</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.gtk.linux.s390x</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.gtk.linux.ppc64</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.motif.linux.x86</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.gtk.linux.ppc</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.photon.qnx.x86</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.motif.aix.ppc</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.motif.aix.ppc64</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.motif.hpux.ia64_32</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.gtk.aix.ppc</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.gtk.aix.ppc64</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.swt.gtk.hpux.ia64_32</artifactId> -->
+               <!-- </exclusion> -->
+
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.carbon.macosx</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.gtk.solaris.sparc</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.gtk.solaris.x86</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.motif.solaris.sparc</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.gtk.linux.s390</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.gtk.linux.s390x</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.gtk.linux.ppc64</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.motif.linux.x86</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.gtk.linux.ppc</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.photon.qnx.x86</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.motif.aix.ppc</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.motif.aix.ppc64</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.motif.hpux.ia64_32</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.gtk.aix.ppc</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.gtk.aix.ppc64</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.equinox.launcher.gtk.hpux.ia64_32</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- <exclusion> -->
+               <!-- <groupId>org.argeo.tp.rcp.e4</groupId> -->
+               <!-- <artifactId>org.eclipse.ui.carbon</artifactId> -->
+               <!-- </exclusion> -->
+               <!-- </exclusions> -->
+               <!-- </dependency> -->
+
+               <!-- From RAP -->
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.emf.common</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.emf.ecore</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.emf.ecore.change</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.emf.ecore.xmi</artifactId>
+               </dependency>
+
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.di</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di.extensions.supplier</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.model.workbench</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.emf.xpath</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.contexts</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.services</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di.annotations</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.ui.services</artifactId>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.rap.e4</groupId>
+                       <artifactId>org.eclipse.e4.core.di.extensions</artifactId>
+               </dependency>
+
+
+               <!-- Eclipse -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons.rcp</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rcp</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui.theme</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- E4 specific -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.e4</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons.rcp</groupId>
+                       <artifactId>org.argeo.cms.e4.rcp</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+
+               <!-- RCP specific -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.commons.rcp</groupId> -->
+               <!-- <artifactId>org.argeo.cms.ui.workbench.rcp</artifactId> -->
+               <!-- <version>2.1.18-SNAPSHOT</version> -->
+               <!-- </dependency> -->
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.commons</groupId> -->
+               <!-- <artifactId>org.argeo.cms.ui.workbench</artifactId> -->
+               <!-- <version>${version.argeo-commons}</version> -->
+               <!-- </dependency> -->
+
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.dep.cms.node</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+                       <type>pom</type>
+               </dependency>
+
+               <!-- SDK -->
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.osgi</artifactId>
+                       <scope>test</scope>
+               </dependency>
+
+       </dependencies>
+       <dependencyManagement>
+               <dependencies>
+                       <dependency>
+                               <groupId>org.argeo.tp</groupId>
+                               <artifactId>argeo-tp-rcp-e4</artifactId>
+                               <version>${version.argeo-tp}</version>
+                               <scope>import</scope>
+                       </dependency>
+               </dependencies>
+       </dependencyManagement>
+       <profiles>
+               <profile>
+                       <id>rpmbuild</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-argeo</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-e4-rcp</name>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>argeo-cms-node</require>
+                                                                               <require>argeo-cms-e4-rcp-tp</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+               <profile>
+                       <id>rpmbuild-tp</id>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <artifactId>maven-assembly-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>prepare-source-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>single</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <descriptorRefs>
+                                                                               <descriptorRef>a2-source-tp</descriptorRef>
+                                                                       </descriptorRefs>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                                       <plugin>
+                                               <groupId>org.codehaus.mojo</groupId>
+                                               <artifactId>rpm-maven-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>rpm-tp</id>
+                                                               <phase>package</phase>
+                                                               <goals>
+                                                                       <goal>rpm</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <name>argeo-cms-e4-rcp-tp</name>
+                                                                       <projversion>${version.argeo-tp}</projversion>
+                                                                       <mappings>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <sources>
+                                                                                               <source>
+                                                                                                       <location>${project.build.directory}/${project.artifactId}-${project.version}-a2-source-tp</location>
+                                                                                                       <includes>
+                                                                                                               <include>**/*.jar</include>
+                                                                                                       </includes>
+                                                                                               </source>
+                                                                                       </sources>
+                                                                               </mapping>
+                                                                               <mapping>
+                                                                                       <directory>/usr/share/osgi/boot</directory>
+                                                                                       <username>root</username>
+                                                                                       <groupname>root</groupname>
+                                                                                       <filemode>644</filemode>
+                                                                                       <directoryIncluded>false</directoryIncluded>
+                                                                                       <dependency>
+                                                                                               <stripVersion>true</stripVersion>
+                                                                                               <includes>
+                                                                                                       <include>org.argeo.tp.rcp.e4:org.eclipse.equinox.launcher</include>
+                                                                                               </includes>
+                                                                                       </dependency>
+                                                                               </mapping>
+                                                                       </mappings>
+                                                                       <requires>
+                                                                               <require>argeo-cms-node-tp</require>
+                                                                       </requires>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
+</project>
diff --git a/rcp/dep/org.argeo.dep.cms.e4.rcp/src/assembly/linux.x86.xml b/rcp/dep/org.argeo.dep.cms.e4.rcp/src/assembly/linux.x86.xml
new file mode 100644 (file)
index 0000000..0b321cd
--- /dev/null
@@ -0,0 +1,59 @@
+<!--
+
+    Copyright (C) 2007-2012 Argeo GmbH
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!-- Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org> Licensed under 
+       the Apache License, Version 2.0 (the "License"); you may not use this file 
+       except in compliance with the License. You may obtain a copy of the License 
+       at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable 
+       law or agreed to in writing, software distributed under the License is distributed 
+       on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
+       express or implied. See the License for the specific language governing permissions 
+       and limitations under the License. -->
+
+<assembly
+       xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+       <id>linux.x86</id>
+       <baseDirectory>argeo-node-ui</baseDirectory>
+       <formats>
+               <format>tar.gz</format>
+       </formats>
+       <dependencySets>
+               <dependencySet>
+                       <unpack>false</unpack>
+                       <outputFileNameMapping>${artifact.artifactId}-${artifact.baseVersion}.${artifact.extension}
+                       </outputFileNameMapping>
+                       <outputDirectory>lib</outputDirectory>
+                       <includes>
+                               <include>*:jar</include>
+                       </includes>
+                       <excludes>
+                               <exclude>org.eclipse.swt:org.eclipse.swt*:jar</exclude>
+                       </excludes>
+               </dependencySet>
+               <dependencySet>
+                       <unpack>false</unpack>
+                       <outputFileNameMapping>${artifact.artifactId}-${artifact.baseVersion}.${artifact.extension}
+                       </outputFileNameMapping>
+                       <outputDirectory>lib</outputDirectory>
+                       <includes>
+                               <include>org.eclipse.swt:org.eclipse.swt.gtk.linux.x86:jar</include>
+                       </includes>
+               </dependencySet>
+       </dependencySets>
+</assembly>
\ No newline at end of file
diff --git a/rcp/dep/org.argeo.dep.cms.e4.rcp/src/assembly/linux.x86_64.xml b/rcp/dep/org.argeo.dep.cms.e4.rcp/src/assembly/linux.x86_64.xml
new file mode 100644 (file)
index 0000000..12a0a32
--- /dev/null
@@ -0,0 +1,59 @@
+<!--
+
+    Copyright (C) 2007-2012 Argeo GmbH
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!-- Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org> Licensed under 
+       the Apache License, Version 2.0 (the "License"); you may not use this file 
+       except in compliance with the License. You may obtain a copy of the License 
+       at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable 
+       law or agreed to in writing, software distributed under the License is distributed 
+       on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
+       express or implied. See the License for the specific language governing permissions 
+       and limitations under the License. -->
+
+<assembly
+       xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+       <id>linux.x86_64</id>
+       <baseDirectory>argeo-node-ui</baseDirectory>
+       <formats>
+               <format>tar.gz</format>
+       </formats>
+       <dependencySets>
+               <dependencySet>
+                       <unpack>false</unpack>
+                       <outputFileNameMapping>${artifact.artifactId}-${artifact.baseVersion}.${artifact.extension}
+                       </outputFileNameMapping>
+                       <outputDirectory>lib</outputDirectory>
+                       <includes>
+                               <include>*:jar</include>
+                       </includes>
+                       <excludes>
+                               <exclude>org.eclipse.swt:org.eclipse.swt*:jar</exclude>
+                       </excludes>
+               </dependencySet>
+               <dependencySet>
+                       <unpack>false</unpack>
+                       <outputFileNameMapping>${artifact.artifactId}-${artifact.baseVersion}.${artifact.extension}
+                       </outputFileNameMapping>
+                       <outputDirectory>lib</outputDirectory>
+                       <includes>
+                               <include>org.eclipse.swt:org.eclipse.swt.gtk.linux.x86_64:jar</include>
+                       </includes>
+               </dependencySet>
+       </dependencySets>
+</assembly>
\ No newline at end of file
diff --git a/rcp/dep/org.argeo.dep.cms.e4.rcp/src/assembly/win32.x86.xml b/rcp/dep/org.argeo.dep.cms.e4.rcp/src/assembly/win32.x86.xml
new file mode 100644 (file)
index 0000000..15cec0d
--- /dev/null
@@ -0,0 +1,59 @@
+<!--
+
+    Copyright (C) 2007-2012 Argeo GmbH
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+            http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!-- Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org> Licensed under 
+       the Apache License, Version 2.0 (the "License"); you may not use this file 
+       except in compliance with the License. You may obtain a copy of the License 
+       at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable 
+       law or agreed to in writing, software distributed under the License is distributed 
+       on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
+       express or implied. See the License for the specific language governing permissions 
+       and limitations under the License. -->
+
+<assembly
+       xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+       <id>win32.x86</id>
+       <baseDirectory>argeo-node-ui</baseDirectory>
+       <formats>
+               <format>zip</format>
+       </formats>
+       <dependencySets>
+               <dependencySet>
+                       <unpack>false</unpack>
+                       <outputFileNameMapping>${artifact.artifactId}-${artifact.baseVersion}.${artifact.extension}
+                       </outputFileNameMapping>
+                       <outputDirectory>lib</outputDirectory>
+                       <includes>
+                               <include>*:jar</include>
+                       </includes>
+                       <excludes>
+                               <exclude>org.eclipse.swt:org.eclipse.swt*:jar</exclude>
+                       </excludes>
+               </dependencySet>
+               <dependencySet>
+                       <unpack>false</unpack>
+                       <outputFileNameMapping>${artifact.artifactId}-${artifact.baseVersion}.${artifact.extension}
+                       </outputFileNameMapping>
+                       <outputDirectory>lib</outputDirectory>
+                       <includes>
+                               <include>org.eclipse.swt:org.eclipse.swt.win32.win32.x86:jar</include>
+                       </includes>
+               </dependencySet>
+       </dependencySets>
+</assembly>
\ No newline at end of file
diff --git a/rcp/dep/pom.xml b/rcp/dep/pom.xml
new file mode 100644 (file)
index 0000000..2db2e50
--- /dev/null
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons.rcp</groupId>
+               <artifactId>argeo-rcp</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>dep</artifactId>
+       <name>RCP Base Dependencies</name>
+       <packaging>pom</packaging>
+       <modules>
+               <module>org.argeo.dep.cms.e4.rcp</module>
+       </modules>
+       <build>
+               <plugins>
+                       <plugin>
+                               <groupId>org.apache.felix</groupId>
+                               <artifactId>maven-bundle-plugin</artifactId>
+                               <configuration>
+                                       <instructions>
+                                               <SLC-ModularDistribution>default</SLC-ModularDistribution>
+                                       </instructions>
+                               </configuration>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.argeo.maven.plugins</groupId>
+                               <artifactId>maven-argeo-osgi-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               <id>generate-descriptors</id>
+                                               <goals>
+                                                       <goal>descriptors</goal>
+                                               </goals>
+                                               <phase>generate-resources</phase>
+                                       </execution>
+                               </executions>
+                       </plugin>
+                       <plugin>
+                               <artifactId>maven-assembly-plugin</artifactId>
+                               <dependencies>
+                                       <dependency>
+                                               <groupId>org.argeo.commons</groupId>
+                                               <artifactId>assembly-descriptors</artifactId>
+                                               <version>2.1.76-SNAPSHOT</version>
+                                       </dependency>
+                               </dependencies>
+                               <configuration>
+                                       <attach>false</attach>
+                               </configuration>
+                       </plugin>
+               </plugins>
+       </build>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.tp.equinox</groupId>
+                       <artifactId>org.eclipse.osgi</artifactId>
+                       <scope>test</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp.sdk</groupId>
+                       <artifactId>org.junit</artifactId>
+                       <scope>test</scope>
+               </dependency>
+       </dependencies>
+       <profiles>
+               <profile>
+                       <id>check-osgi</id>
+                       <dependencies>
+                               <dependency>
+                                       <groupId>org.argeo.commons</groupId>
+                                       <artifactId>org.argeo.osgi.boot</artifactId>
+                                       <version>${version.argeo-commons}</version>
+                                       <scope>test</scope>
+                               </dependency>
+                       </dependencies>
+                       <build>
+                               <plugins>
+                                       <plugin>
+                                               <groupId>org.argeo.maven.plugins</groupId>
+                                               <artifactId>maven-argeo-osgi-plugin</artifactId>
+                                               <executions>
+                                                       <execution>
+                                                               <id>check-osgi</id>
+                                                               <phase>test</phase>
+                                                               <goals>
+                                                                       <goal>equinox</goal>
+                                                               </goals>
+                                                               <configuration>
+                                                                       <onlyCheck>true</onlyCheck>
+                                                               </configuration>
+                                                       </execution>
+                                               </executions>
+                                       </plugin>
+                               </plugins>
+                       </build>
+               </profile>
+       </profiles>
+</project>
\ No newline at end of file
diff --git a/rcp/dist/argeo-companion/rpm/etc/argeo-companion/argeo-companion.ini b/rcp/dist/argeo-companion/rpm/etc/argeo-companion/argeo-companion.ini
new file mode 100644 (file)
index 0000000..444c39d
--- /dev/null
@@ -0,0 +1,25 @@
+osgi.bundles=org.argeo.osgi.boot@start
+osgi.clean=true
+
+argeo.osgi.start.2.rcp=\
+org.eclipse.core.runtime
+
+argeo.osgi.start.2.node=\
+org.eclipse.equinox.metatype,\
+org.eclipse.equinox.ds,\
+org.eclipse.equinox.cm,\
+
+argeo.osgi.start.3.node=\
+org.argeo.cms
+
+applicationXMI=org.argeo.cms.e4.rcp/argeo-companion.e4xmi
+lifeCycleURI=bundleclass://org.argeo.cms.e4.rcp/org.argeo.cms.e4.rcp.CmsRcpLifeCycle
+clearPersistedState=true
+#argeo.cms.desktop.inTray=true
+
+# Remote node:
+#argeo.node.repo.labeledUri=http://root:demo@localhost:7070/jcr/node
+
+log4j.configuration=file:../../log4j.properties
+argeo.node.useradmin.uris=os:///
+eclipse.application=org.argeo.cms.e4.rcp.CmsE4Application
diff --git a/rcp/dist/argeo-companion/rpm/etc/argeo-companion/log4j.properties b/rcp/dist/argeo-companion/rpm/etc/argeo-companion/log4j.properties
new file mode 100644 (file)
index 0000000..13f949f
--- /dev/null
@@ -0,0 +1,32 @@
+log4j.rootLogger=WARN, development
+
+## Levels
+log4j.logger.org.argeo=DEBUG
+log4j.logger.org.argeo.jackrabbit.remote.ExtendedDispatcherServlet=WARN
+log4j.logger.org.argeo.server.webextender.TomcatDeployer=INFO
+
+#log4j.logger.org.springframework.security=DEBUG
+#log4j.logger.org.apache.commons.exec=DEBUG
+#log4j.logger.org.apache.jackrabbit.webdav=DEBUG
+#log4j.logger.org.apache.jackrabbit.remote=DEBUG
+#log4j.logger.org.apache.jackrabbit.core.observation=DEBUG
+
+log4j.logger.org.apache.catalina=INFO
+log4j.logger.org.apache.coyote=INFO
+
+log4j.logger.org.apache.directory=INFO
+log4j.logger.org.apache.directory.server=ERROR
+log4j.logger.org.apache.jackrabbit.core.query.lucene=ERROR
+
+## Appenders
+# console is set to be a ConsoleAppender.
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+
+# console uses PatternLayout.
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern= %-5p %d{ISO8601} %m - %c - [%t]%n
+
+# development appender (slow!)
+log4j.appender.development=org.apache.log4j.ConsoleAppender
+log4j.appender.development.layout=org.apache.log4j.PatternLayout
+log4j.appender.development.layout.ConversionPattern=%d{HH:mm:ss} [%16.16t] %5p %m (%F:%L) %c%n
diff --git a/rcp/dist/argeo-companion/rpm/usr/bin/argeo-companion b/rcp/dist/argeo-companion/rpm/usr/bin/argeo-companion
new file mode 100755 (executable)
index 0000000..ab25dc7
--- /dev/null
@@ -0,0 +1,54 @@
+#!/bin/sh
+APP=argeo-companion
+
+JVM=java
+
+# Directories and files
+CONF_DIR=/etc/$APP
+CONF_DIRS=/etc/$APP/conf.d
+BASE_POLICY_ALL=/usr/share/$APP/all.policy
+BASE_CONFIG_INI=/usr/share/$APP/config.ini
+
+EXEC_DIR=$HOME/.local/share/argeo-companion
+DATA_DIR=$EXEC_DIR/data
+CONF_RW=$EXEC_DIR/state
+CONFIG_INI=$CONF_RW/config.ini
+
+OSGI_INSTALL_AREA=/usr/share/osgi/boot
+OSGI_FRAMEWORK=$OSGI_INSTALL_AREA/org.eclipse.osgi.jar
+ECLIPSE_LAUNCHER=$OSGI_INSTALL_AREA/org.eclipse.equinox.launcher.jar
+
+# Overwrite variables
+if [ -f $CONF_DIR/settings.sh ];then
+       . $CONF_DIR/settings.sh
+fi
+
+RETVAL=0
+
+start() {
+       mkdir -p $CONF_RW
+       mkdir -p $DATA_DIR
+
+    # Merge config files
+    printf "## Equinox configuration - Generated by /usr/sbin/nodectl ##\n\n" > $CONFIG_INI
+#    cat $BASE_CONFIG_INI >> $CONFIG_INI
+    printf "\n##\n## $CONF_DIR/$APP.ini\n##\n\n" >> $CONFIG_INI
+    cat $CONF_DIR/$APP.ini >> $CONFIG_INI
+    for file in `ls -v $CONF_DIRS/*.ini`; do
+            printf "\n##\n## $file\n##\n\n" >> $CONFIG_INI
+            cat $file >> $CONFIG_INI
+    done;
+
+#              $JAVA_OPTS -jar $OSGI_FRAMEWORK \
+
+       cd $EXEC_DIR
+       $JVM \
+               -Dlog4j.configuration="file:$CONF_DIR/log4j.properties" \
+               -Dosgi.framework=$OSGI_FRAMEWORK \
+               $JAVA_OPTS -classpath $ECLIPSE_LAUNCHER org.eclipse.equinox.launcher.Main \
+               -configuration "$CONF_RW" \
+               -data "$DATA_DIR"
+}
+
+start
+
diff --git a/rcp/org.argeo.cms.e4.rcp/.classpath b/rcp/org.argeo.cms.e4.rcp/.classpath
new file mode 100644 (file)
index 0000000..eca7bdb
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
+       <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/rcp/org.argeo.cms.e4.rcp/.gitignore b/rcp/org.argeo.cms.e4.rcp/.gitignore
new file mode 100644 (file)
index 0000000..09e3bc9
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/target/
diff --git a/rcp/org.argeo.cms.e4.rcp/.project b/rcp/org.argeo.cms.e4.rcp/.project
new file mode 100644 (file)
index 0000000..64d5619
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.cms.e4.rcp</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/rcp/org.argeo.cms.e4.rcp/.settings/org.eclipse.jdt.core.prefs b/rcp/org.argeo.cms.e4.rcp/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..0c68a61
--- /dev/null
@@ -0,0 +1,7 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
+org.eclipse.jdt.core.compiler.compliance=1.8
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.8
diff --git a/rcp/org.argeo.cms.e4.rcp/.settings/org.eclipse.pde.core.prefs b/rcp/org.argeo.cms.e4.rcp/.settings/org.eclipse.pde.core.prefs
new file mode 100644 (file)
index 0000000..f29e940
--- /dev/null
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+pluginProject.extensions=false
+resolve.requirebundle=false
diff --git a/rcp/org.argeo.cms.e4.rcp/META-INF/.gitignore b/rcp/org.argeo.cms.e4.rcp/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/rcp/org.argeo.cms.e4.rcp/argeo-companion.e4xmi b/rcp/org.argeo.cms.e4.rcp/argeo-companion.e4xmi
new file mode 100644 (file)
index 0000000..abce62a
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="ASCII"?>
+<application:Application xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:advanced="http://www.eclipse.org/ui/2010/UIModel/application/ui/advanced" xmlns:application="http://www.eclipse.org/ui/2010/UIModel/application" xmlns:basic="http://www.eclipse.org/ui/2010/UIModel/application/ui/basic" xmi:id="_c4iAgCnCEei1F8sdBz8Mpw" elementId="org.argeo.cms.e4.rcp.application">
+  <children xsi:type="basic:TrimmedWindow" xmi:id="_hSGBwCnCEei1F8sdBz8Mpw" elementId="org.argeo.cms.e4.rcp.trimmedwindow.argeocompanion" label="Argeo Companion">
+    <children xsi:type="advanced:PerspectiveStack" xmi:id="_nxzQICnCEei1F8sdBz8Mpw" elementId="org.argeo.cms.e4.rcp.perspectivestack.0">
+      <children xsi:type="advanced:Perspective" xmi:id="_oI_oICnCEei1F8sdBz8Mpw" elementId="org.argeo.cms.e4.rcp.perspective.cmsadmin" label="CMS Admin">
+        <children xsi:type="basic:PartSashContainer" xmi:id="_qc16ECnCEei1F8sdBz8Mpw" elementId="org.argeo.cms.e4.rcp.partsashcontainer.0" horizontal="true">
+          <children xsi:type="basic:PartStack" xmi:id="_RE87kDsXEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.rcp.partstack.1">
+            <children xsi:type="basic:Part" xmi:id="_V1WvgDsXEeiUntFYWh-hFg" elementId="org.argeo.cms.e4.rcp.part.files" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.files.NodeFsBrowserView" label="Files"/>
+            <children xsi:type="basic:Part" xmi:id="_vOqDQCnCEei1F8sdBz8Mpw" elementId="org.argeo.cms.e4.rcp.part.jcr" containerData="4000" contributionURI="bundleclass://org.argeo.cms.e4/org.argeo.cms.e4.jcr.JcrBrowserView" label="JCR"/>
+          </children>
+          <children xsi:type="basic:PartStack" xmi:id="_0eRiwCnCEei1F8sdBz8Mpw" elementId="org.argeo.cms.e4.rcp.partstack.0" containerData="6000"/>
+        </children>
+      </children>
+    </children>
+  </children>
+  <addons xmi:id="_c4iAgSnCEei1F8sdBz8Mpw" elementId="org.eclipse.e4.core.commands.service" contributionURI="bundleclass://org.eclipse.e4.core.commands/org.eclipse.e4.core.commands.CommandServiceAddon"/>
+  <addons xmi:id="_c4iAginCEei1F8sdBz8Mpw" elementId="org.eclipse.e4.ui.contexts.service" contributionURI="bundleclass://org.eclipse.e4.ui.services/org.eclipse.e4.ui.services.ContextServiceAddon"/>
+  <addons xmi:id="_c4iAgynCEei1F8sdBz8Mpw" elementId="org.eclipse.e4.ui.bindings.service" contributionURI="bundleclass://org.eclipse.e4.ui.bindings/org.eclipse.e4.ui.bindings.BindingServiceAddon"/>
+  <addons xmi:id="_c4iAhCnCEei1F8sdBz8Mpw" elementId="org.eclipse.e4.ui.workbench.commands.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.CommandProcessingAddon"/>
+  <addons xmi:id="_c4iAhSnCEei1F8sdBz8Mpw" elementId="org.eclipse.e4.ui.workbench.contexts.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.ContextProcessingAddon"/>
+  <addons xmi:id="_c4iAhinCEei1F8sdBz8Mpw" elementId="org.eclipse.e4.ui.workbench.bindings.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench.swt/org.eclipse.e4.ui.workbench.swt.util.BindingProcessingAddon"/>
+  <addons xmi:id="_c4iAhynCEei1F8sdBz8Mpw" elementId="org.eclipse.e4.ui.workbench.handler.model" contributionURI="bundleclass://org.eclipse.e4.ui.workbench/org.eclipse.e4.ui.internal.workbench.addons.HandlerProcessingAddon"/>
+</application:Application>
diff --git a/rcp/org.argeo.cms.e4.rcp/bnd.bnd b/rcp/org.argeo.cms.e4.rcp/bnd.bnd
new file mode 100644 (file)
index 0000000..7759c84
--- /dev/null
@@ -0,0 +1,8 @@
+Bundle-SymbolicName: org.argeo.cms.e4.rcp;singleton=true
+
+Require-Bundle: org.eclipse.core.runtime
+
+Import-Package: org.argeo.node,\
+!org.eclipse.core.runtime,\
+org.eclipse.swt,\
+*
diff --git a/rcp/org.argeo.cms.e4.rcp/build.properties b/rcp/org.argeo.cms.e4.rcp/build.properties
new file mode 100644 (file)
index 0000000..355413e
--- /dev/null
@@ -0,0 +1,5 @@
+output.. = bin/
+bin.includes = META-INF/,\
+               .,\
+               argeo-companion.e4xmi
+source.. = src/
diff --git a/rcp/org.argeo.cms.e4.rcp/plugin.xml b/rcp/org.argeo.cms.e4.rcp/plugin.xml
new file mode 100644 (file)
index 0000000..3e6896b
--- /dev/null
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?eclipse version="3.4"?>
+<plugin>
+   <extension
+         id="CmsE4Application"
+         name="CMS E4 Application"
+         point="org.eclipse.core.runtime.applications">
+      <application
+            cardinality="singleton-global"
+            thread="main"
+            visible="true">
+         <run class="org.argeo.cms.e4.rcp.CmsE4Application"></run>
+      </application>
+   </extension>
+</plugin>
diff --git a/rcp/org.argeo.cms.e4.rcp/pom.xml b/rcp/org.argeo.cms.e4.rcp/pom.xml
new file mode 100644 (file)
index 0000000..dfe0d32
--- /dev/null
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons.rcp</groupId>
+               <artifactId>argeo-rcp</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.cms.e4.rcp</artifactId>
+       <name>CMS E4 RCP</name>
+       <packaging>jar</packaging>
+       <dependencies>
+               <!-- Base Argeo UI -->
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.cms.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <!-- RCP specific -->
+               <dependency>
+                       <groupId>org.argeo.commons.rcp</groupId>
+                       <artifactId>org.argeo.eclipse.ui.rcp</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/rcp/org.argeo.cms.e4.rcp/src/org/argeo/cms/e4/rcp/CmsE4Application.java b/rcp/org.argeo.cms.e4.rcp/src/org/argeo/cms/e4/rcp/CmsE4Application.java
new file mode 100644 (file)
index 0000000..f8c82cb
--- /dev/null
@@ -0,0 +1,194 @@
+package org.argeo.cms.e4.rcp;
+
+import java.security.PrivilegedExceptionAction;
+
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.argeo.cms.CmsException;
+import org.argeo.cms.auth.CurrentUser;
+import org.argeo.cms.ui.CmsImageManager;
+import org.argeo.cms.ui.CmsView;
+import org.argeo.cms.ui.UxContext;
+import org.argeo.cms.util.SimpleUxContext;
+import org.argeo.cms.widgets.auth.CmsLoginShell;
+import org.argeo.node.NodeConstants;
+import org.eclipse.core.runtime.IConfigurationElement;
+import org.eclipse.core.runtime.IExtension;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.equinox.app.IApplication;
+import org.eclipse.equinox.app.IApplicationContext;
+import org.eclipse.swt.widgets.Display;
+
+public class CmsE4Application implements IApplication, CmsView {
+       private LoginContext loginContext;
+       private IApplication e4Application;
+       private UxContext uxContext;
+
+       @Override
+       public Object start(IApplicationContext context) throws Exception {
+               Subject subject = new Subject();
+               Display display = createDisplay();
+               CmsLoginShell loginShell = new CmsLoginShell(this);
+               // TODO customize CmsLoginShell to be smaller and centered
+               loginShell.setSubject(subject);
+               try {
+                       // try pre-auth
+                       loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_USER, subject, loginShell);
+                       loginContext.login();
+               } catch (LoginException e) {
+                       e.printStackTrace();
+                       loginShell.createUi();
+                       loginShell.open();
+
+                       while (!loginShell.getShell().isDisposed()) {
+                               if (!display.readAndDispatch())
+                                       display.sleep();
+                       }
+               }
+               if (CurrentUser.getUsername(getSubject()) == null)
+                       throw new CmsException("Cannot log in");
+
+               // try {
+               // CallbackHandler callbackHandler = new DefaultLoginDialog(
+               // display.getActiveShell());
+               // loginContext = new LoginContext(
+               // NodeConstants.LOGIN_CONTEXT_SINGLE_USER, subject,
+               // callbackHandler);
+               // } catch (LoginException e1) {
+               // throw new CmsException("Cannot initialize login context", e1);
+               // }
+               //
+               // // login
+               // try {
+               // loginContext.login();
+               // subject = loginContext.getSubject();
+               // } catch (LoginException e) {
+               // e.printStackTrace();
+               // display.dispose();
+               // try {
+               // Thread.sleep(2000);
+               // } catch (InterruptedException e1) {
+               // // silent
+               // }
+               // return null;
+               // }
+
+               uxContext = new SimpleUxContext();
+
+               e4Application = getApplication(null);
+               Object res = Subject.doAs(subject, new PrivilegedExceptionAction<Object>() {
+
+                       @Override
+                       public Object run() throws Exception {
+                               return e4Application.start(context);
+                       }
+
+               });
+               return res;
+       }
+
+       @Override
+       public void stop() {
+               if (e4Application != null)
+                       e4Application.stop();
+       }
+
+       static IApplication getApplication(String[] args) {
+               IExtension extension = Platform.getExtensionRegistry().getExtension(Platform.PI_RUNTIME,
+                               Platform.PT_APPLICATIONS, "org.eclipse.e4.ui.workbench.swt.E4Application");
+               try {
+                       IConfigurationElement[] elements = extension.getConfigurationElements();
+                       if (elements.length > 0) {
+                               IConfigurationElement[] runs = elements[0].getChildren("run");
+                               if (runs.length > 0) {
+                                       Object runnable;
+                                       runnable = runs[0].createExecutableExtension("class");
+                                       if (runnable instanceof IApplication)
+                                               return (IApplication) runnable;
+                               }
+                       }
+               } catch (Exception e) {
+                       throw new IllegalStateException("Cannot find e4 application", e);
+               }
+               throw new IllegalStateException("Cannot find e4 application");
+       }
+
+       public static Display createDisplay() {
+               Display.setAppName("Argeo CMS RCP");
+
+               // create the display
+               Display newDisplay = Display.getCurrent();
+               if (newDisplay == null) {
+                       newDisplay = new Display();
+               }
+               // Set the priority higher than normal so as to be higher
+               // than the JobManager.
+               Thread.currentThread().setPriority(Math.min(Thread.MAX_PRIORITY, Thread.NORM_PRIORITY + 1));
+               return newDisplay;
+       }
+
+       //
+       // CMS VIEW
+       //
+
+       @Override
+       public UxContext getUxContext() {
+               return uxContext;
+       }
+
+       @Override
+       public void navigateTo(String state) {
+               // TODO Auto-generated method stub
+
+       }
+
+       @Override
+       public void authChange(LoginContext loginContext) {
+               if (loginContext == null)
+                       throw new CmsException("Login context cannot be null");
+               // logout previous login context
+               // if (this.loginContext != null)
+               // try {
+               // this.loginContext.logout();
+               // } catch (LoginException e1) {
+               // System.err.println("Could not log out: " + e1);
+               // }
+               this.loginContext = loginContext;
+       }
+
+       @Override
+       public void logout() {
+               if (loginContext == null)
+                       throw new CmsException("Login context should not bet null");
+               try {
+                       CurrentUser.logoutCmsSession(loginContext.getSubject());
+                       loginContext.logout();
+               } catch (LoginException e) {
+                       throw new CmsException("Cannot log out", e);
+               }
+       }
+
+       @Override
+       public void exception(Throwable e) {
+               // TODO Auto-generated method stub
+
+       }
+
+       @Override
+       public CmsImageManager getImageManager() {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+       protected Subject getSubject() {
+               return loginContext.getSubject();
+       }
+
+       @Override
+       public boolean isAnonymous() {
+               return CurrentUser.isAnonymous(getSubject());
+       }
+
+}
diff --git a/rcp/org.argeo.cms.e4.rcp/src/org/argeo/cms/e4/rcp/CmsRcpLifeCycle.java b/rcp/org.argeo.cms.e4.rcp/src/org/argeo/cms/e4/rcp/CmsRcpLifeCycle.java
new file mode 100644 (file)
index 0000000..1d38fe7
--- /dev/null
@@ -0,0 +1,27 @@
+package org.argeo.cms.e4.rcp;
+
+import org.eclipse.e4.core.contexts.IEclipseContext;
+import org.eclipse.e4.ui.workbench.lifecycle.PostContextCreate;
+import org.eclipse.e4.ui.workbench.lifecycle.PreSave;
+import org.eclipse.e4.ui.workbench.lifecycle.ProcessAdditions;
+import org.eclipse.e4.ui.workbench.lifecycle.ProcessRemovals;
+
+@SuppressWarnings("restriction")
+public class CmsRcpLifeCycle {
+
+       @PostContextCreate
+       void postContextCreate(IEclipseContext workbenchContext) {
+       }
+
+       @PreSave
+       void preSave(IEclipseContext workbenchContext) {
+       }
+
+       @ProcessAdditions
+       void processAdditions(IEclipseContext workbenchContext) {
+       }
+
+       @ProcessRemovals
+       void processRemovals(IEclipseContext workbenchContext) {
+       }
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/.classpath b/rcp/org.argeo.eclipse.ui.rcp/.classpath
new file mode 100644 (file)
index 0000000..457b115
--- /dev/null
@@ -0,0 +1,9 @@
+<?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-1.8" />
+       <classpathentry kind="output" path="bin" />
+</classpath>
diff --git a/rcp/org.argeo.eclipse.ui.rcp/.gitignore b/rcp/org.argeo.eclipse.ui.rcp/.gitignore
new file mode 100644 (file)
index 0000000..0f63015
--- /dev/null
@@ -0,0 +1,2 @@
+/target/
+/bin/
diff --git a/rcp/org.argeo.eclipse.ui.rcp/.project b/rcp/org.argeo.eclipse.ui.rcp/.project
new file mode 100644 (file)
index 0000000..ef2dc2d
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>org.argeo.eclipse.ui.rcp</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.pde.PluginNature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/rcp/org.argeo.eclipse.ui.rcp/META-INF/.gitignore b/rcp/org.argeo.eclipse.ui.rcp/META-INF/.gitignore
new file mode 100644 (file)
index 0000000..4854a41
--- /dev/null
@@ -0,0 +1 @@
+/MANIFEST.MF
diff --git a/rcp/org.argeo.eclipse.ui.rcp/bnd.bnd b/rcp/org.argeo.eclipse.ui.rcp/bnd.bnd
new file mode 100644 (file)
index 0000000..bcfde8e
--- /dev/null
@@ -0,0 +1,19 @@
+Import-Package: org.apache.commons.io,\
+                               org.eclipse.core.commands,\
+                               org.springframework.beans.factory,\
+                               org.springframework.core.io.support,\
+                               org.argeo.eclipse.ui.utils,\
+                               !org.eclipse.core.runtime,\
+                               !org.eclipse.ui.plugin,\
+                               org.eclipse.swt,\
+                               *
+
+Export-Package: org.argeo.*,\
+org.eclipse.rap.fileupload.*;version="3.4",\
+org.eclipse.rap.rwt.*;version="3.4"
+
+# Was !org.eclipse.core.commands,\ why ?
+
+#Bundle-Activator: org.argeo.eclipse.ui.ArgeoUiPlugin
+#Bundle-ActivationPolicy: lazy
+#Ignore-Package: org.eclipse.core.commands
\ No newline at end of file
diff --git a/rcp/org.argeo.eclipse.ui.rcp/build.properties b/rcp/org.argeo.eclipse.ui.rcp/build.properties
new file mode 100644 (file)
index 0000000..c6b651a
--- /dev/null
@@ -0,0 +1,3 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/
diff --git a/rcp/org.argeo.eclipse.ui.rcp/pom.xml b/rcp/org.argeo.eclipse.ui.rcp/pom.xml
new file mode 100644 (file)
index 0000000..6469ea9
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons.rcp</groupId>
+               <artifactId>argeo-rcp</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <artifactId>org.argeo.eclipse.ui.rcp</artifactId>
+       <name>Eclipse UI RCP</name>
+       <description>Provide RCP specific classes and behaviour in order to enable single sourcing</description>
+       <packaging>jar</packaging>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.eclipse.ui</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.commons</groupId>
+                       <artifactId>org.argeo.util</artifactId>
+                       <version>2.1.76-SNAPSHOT</version>
+               </dependency>
+       </dependencies>
+</project>
\ No newline at end of file
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpClient.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpClient.java
new file mode 100644 (file)
index 0000000..0d9ce48
--- /dev/null
@@ -0,0 +1,44 @@
+package org.argeo.eclipse.ui.rcp.internal.rwt;
+
+import org.eclipse.rap.rwt.client.Client;
+import org.eclipse.rap.rwt.client.service.BrowserNavigation;
+import org.eclipse.rap.rwt.client.service.BrowserNavigationListener;
+import org.eclipse.rap.rwt.client.service.ClientService;
+import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
+
+public class RcpClient implements Client {
+
+       @Override
+       public <T extends ClientService> T getService(Class<T> type) {
+               if (type.isAssignableFrom(JavaScriptExecutor.class))
+                       return (T) javaScriptExecutor;
+               else if (type.isAssignableFrom(BrowserNavigation.class))
+                       return (T) browserNavigation;
+               else
+                       return null;
+       }
+
+       private JavaScriptExecutor javaScriptExecutor = new JavaScriptExecutor() {
+
+               @Override
+               public void execute(String code) {
+                       // TODO Auto-generated method stub
+
+               }
+       };
+       private BrowserNavigation browserNavigation = new BrowserNavigation() {
+
+               @Override
+               public void pushState(String state, String title) {
+                       // TODO Auto-generated method stub
+
+               }
+
+               @Override
+               public void addBrowserNavigationListener(
+                               BrowserNavigationListener listener) {
+                       // TODO Auto-generated method stub
+
+               }
+       };
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpResourceManager.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/rcp/internal/rwt/RcpResourceManager.java
new file mode 100644 (file)
index 0000000..91109a9
--- /dev/null
@@ -0,0 +1,46 @@
+package org.argeo.eclipse.ui.rcp.internal.rwt;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.commons.io.IOUtils;
+import org.eclipse.rap.rwt.service.ResourceManager;
+
+public class RcpResourceManager implements ResourceManager {
+       private Map<String, byte[]> register = Collections
+                       .synchronizedMap(new TreeMap<String, byte[]>());
+
+       @Override
+       public void register(String name, InputStream in) {
+               try {
+                       register.put(name, IOUtils.toByteArray(in));
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot register " + name, e);
+               }
+       }
+
+       @Override
+       public boolean unregister(String name) {
+               return register.remove(name) != null;
+       }
+
+       @Override
+       public InputStream getRegisteredContent(String name) {
+               return new ByteArrayInputStream(register.get(name));
+       }
+
+       @Override
+       public String getLocation(String name) {
+               return name;
+       }
+
+       @Override
+       public boolean isRegistered(String name) {
+               return register.containsKey(name);
+       }
+
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/DefaultNLS.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/DefaultNLS.java
new file mode 100644 (file)
index 0000000..1d3cd29
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.specific;
+
+/** RCP specific {@link NLS} to be extended */
+public class DefaultNLS {// extends NLS {
+//     public final static String DEFAULT_BUNDLE_LOCATION = "/properties/plugin";
+//
+//     public DefaultNLS() {
+//             this(DEFAULT_BUNDLE_LOCATION);
+//     }
+//
+//     public DefaultNLS(String bundleName) {
+//             NLS.initializeMessages(bundleName, getClass());
+//     }
+}
\ No newline at end of file
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/EclipseUiSpecificUtils.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/EclipseUiSpecificUtils.java
new file mode 100644 (file)
index 0000000..7ed0a4f
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.specific;
+
+import org.eclipse.jface.viewers.ColumnViewer;
+import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
+import org.eclipse.jface.viewers.Viewer;
+
+/** Static utilities to bridge differences between RCP and RAP */
+public class EclipseUiSpecificUtils {
+       /**
+        * TootlTip support is supported for {@link ColumnViewer} in RCP
+        * 
+        * @see ColumnViewerToolTipSupport#enableFor(Viewer)
+        */
+       public static void enableToolTipSupport(Viewer viewer) {
+               if (viewer instanceof ColumnViewer)
+                       ColumnViewerToolTipSupport.enableFor((ColumnViewer) viewer);
+       }
+
+       private EclipseUiSpecificUtils() {
+       }
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/OpenFile.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/OpenFile.java
new file mode 100644 (file)
index 0000000..704079f
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2007-2012 Argeo GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *         http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.argeo.eclipse.ui.specific;
+
+import static org.argeo.eclipse.ui.utils.SingleSourcingConstants.FILE_SCHEME;
+import static org.argeo.eclipse.ui.utils.SingleSourcingConstants.SCHEME_HOST_SEPARATOR;
+
+import java.awt.Desktop;
+import java.io.File;
+import java.io.IOException;
+
+import org.argeo.eclipse.ui.utils.SingleSourcingConstants;
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+
+/**
+ * RCP specific command handler to open a file.
+ * 
+ * The parameter "URI" is used to determine the correct method to open it.
+ * 
+ * Various instances of this handler with different command ID might coexist in
+ * order to provide context specific open file services.
+ * 
+ */
+public class OpenFile extends AbstractHandler {
+       // private final static Log log = LogFactory.getLog(OpenFile.class);
+       public final static String ID = SingleSourcingConstants.OPEN_FILE_CMD_ID;
+       public final static String PARAM_FILE_NAME = SingleSourcingConstants.PARAM_FILE_NAME;
+       public final static String PARAM_FILE_URI = SingleSourcingConstants.PARAM_FILE_URI;
+
+       public Object execute(ExecutionEvent event) throws ExecutionException {
+               String fileUri = event.getParameter(PARAM_FILE_URI);
+
+               // sanity check
+               if (fileUri == null || "".equals(fileUri.trim()))
+                       return null;
+
+               Desktop desktop = null;
+               if (Desktop.isDesktopSupported()) {
+                       desktop = Desktop.getDesktop();
+               }
+
+               File file = getFileFromUri(fileUri);
+               if (file != null)
+                       try {
+                               desktop.open(file);
+                       } catch (IOException e) {
+                               throw new SingleSourcingException("Unable to open file with URI: " + fileUri, e);
+                       }
+               return null;
+       }
+
+       protected File getFileFromUri(String uri) {
+               if (uri.startsWith(FILE_SCHEME)) {
+                       String path = uri.substring((FILE_SCHEME + SCHEME_HOST_SEPARATOR).length());
+                       return new File(path);
+               }
+               return null;
+       }
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/SingleSourcingException.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/SingleSourcingException.java
new file mode 100644 (file)
index 0000000..9b75690
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.eclipse.ui.specific;
+
+/** Exception related to SWT/RWT single sourcing. */
+public class SingleSourcingException extends RuntimeException {
+       private static final long serialVersionUID = -727700418055348468L;
+
+       public SingleSourcingException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+       public SingleSourcingException(String message) {
+               super(message);
+       }
+
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/UiContext.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/argeo/eclipse/ui/specific/UiContext.java
new file mode 100644 (file)
index 0000000..bb7cea2
--- /dev/null
@@ -0,0 +1,53 @@
+package org.argeo.eclipse.ui.specific;
+
+import java.util.Locale;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.swt.widgets.Display;
+
+/** Singleton class providing single sources infos about the UI context. */
+public class UiContext {
+
+       public static HttpServletRequest getHttpRequest() {
+               return null;
+       }
+
+       public static HttpServletResponse getHttpResponse() {
+               return null;
+       }
+
+       public static Locale getLocale() {
+               return Locale.getDefault();
+       }
+
+       public static void setLocale(Locale locale) {
+               Locale.setDefault(locale);
+       }
+
+       /** Can always be null */
+       @SuppressWarnings("unchecked")
+       public static <T> T getData(String key) {
+               Display display = getDisplay();
+               if (display == null)
+                       return null;
+               return (T) display.getData(key);
+       }
+
+       public static void setData(String key, Object value) {
+               Display display = getDisplay();
+               if (display == null)
+                       throw new SingleSourcingException(
+                                       "Not display available in RAP context");
+               display.setData(key, value);
+       }
+
+       private static Display getDisplay() {
+               return Display.getCurrent();
+       }
+
+       private UiContext() {
+       }
+
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileDetails.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileDetails.java
new file mode 100644 (file)
index 0000000..fbb36dd
--- /dev/null
@@ -0,0 +1,9 @@
+package org.eclipse.rap.fileupload;
+
+public interface FileDetails {
+       String getContentType();
+
+       long getContentLength();
+
+       String getFileName();
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadEvent.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadEvent.java
new file mode 100644 (file)
index 0000000..a745280
--- /dev/null
@@ -0,0 +1,21 @@
+package org.eclipse.rap.fileupload;
+
+import java.util.EventObject;
+
+public abstract class FileUploadEvent extends EventObject {
+
+       private static final long serialVersionUID = 1L;
+
+       protected FileUploadEvent(FileUploadHandler source) {
+               super(source);
+       }
+
+       public abstract FileDetails[] getFileDetails();
+
+       public abstract long getContentLength();
+
+       public abstract long getBytesRead();
+
+       public abstract Exception getException();
+
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadHandler.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadHandler.java
new file mode 100644 (file)
index 0000000..7d89300
--- /dev/null
@@ -0,0 +1,38 @@
+/*******************************************************************************
+ * Copyright (c) 2011, 2012 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.fileupload;
+
+/**
+ * A file upload handler is used to accept file uploads from a client. After
+ * creating a file upload handler, the server will accept file uploads to the
+ * URL returned by <code>getUploadUrl()</code>. Upload listeners can be attached
+ * to react on progress. When the upload has finished, a FileUploadHandler has
+ * to be disposed of by calling its <code>dispose()</code> method.
+ *
+ * @noextend This class is not intended to be subclassed by clients.
+ */
+public class FileUploadHandler {
+
+       public FileUploadHandler(FileUploadReceiver fileUploadReceiver) {
+       }
+
+       public void dispose() {
+
+       }
+
+       public void addUploadListener(FileUploadListener listener) {
+
+       }
+
+       public String getUploadUrl() {
+               return null;
+       }
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadListener.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadListener.java
new file mode 100644 (file)
index 0000000..b59fd39
--- /dev/null
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * Copyright (c) 2011, 2012 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.fileupload;
+
+import org.eclipse.swt.widgets.Display;
+
+
+/**
+ * Listener to react on progress and completion of a file upload.
+ * <p>
+ * <strong>Note:</strong> This listener will be called from a different thread than the UI thread.
+ * Implementations must use {@link Display#asyncExec(Runnable)} to access the UI.
+ * </p>
+ *
+ * @see FileUploadEvent
+ */
+public interface FileUploadListener {
+
+  /**
+   * Called when new information about an in-progress upload is available.
+   *
+   * @param event event object that contains information about the uploaded file
+   * @see FileUploadEvent#getBytesRead()
+   */
+  void uploadProgress( FileUploadEvent event );
+
+  /**
+   * Called when a file upload has finished successfully.
+   *
+   * @param event event object that contains information about the uploaded file
+   * @see FileUploadEvent
+   */
+  void uploadFinished( FileUploadEvent event );
+
+  /**
+   * Called when a file upload failed.
+   *
+   * @param event event object that contains information about the uploaded file
+   * @see FileUploadEvent#getErrorMessage()
+   */
+  void uploadFailed( FileUploadEvent event );
+
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadReceiver.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/fileupload/FileUploadReceiver.java
new file mode 100644 (file)
index 0000000..3f4cf47
--- /dev/null
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2011, 2013 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.fileupload;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+
+/**
+ * Instances of this interface are responsible for reading and processing the data from a file
+ * upload.
+ */
+public abstract class FileUploadReceiver {
+
+  /**
+   * Reads and processes all data from the provided input stream.
+   *
+   * @param stream the stream to read from
+   * @param details the details of the uploaded file like file name, content-type and size
+   * @throws IOException if an input / output error occurs
+   */
+  public abstract void receive( InputStream stream, FileDetails details ) throws IOException;
+
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/RWT.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/RWT.java
new file mode 100644 (file)
index 0000000..1219641
--- /dev/null
@@ -0,0 +1,43 @@
+package org.eclipse.rap.rwt;
+
+import java.util.Locale;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.argeo.eclipse.ui.rcp.internal.rwt.RcpClient;
+import org.argeo.eclipse.ui.rcp.internal.rwt.RcpResourceManager;
+import org.eclipse.rap.rwt.client.Client;
+import org.eclipse.rap.rwt.service.ResourceManager;
+
+public class RWT {
+       public final static String CUSTOM_VARIANT = "argeo-rcp:CUSTOM_VARIANT";
+       public final static String MARKUP_ENABLED = "argeo-rcp:MARKUP_ENABLED";
+       public final static String CUSTOM_ITEM_HEIGHT = "argeo-rcp:CUSTOM_ITEM_HEIGHT";
+       public final static String ACTIVE_KEYS = "argeo-rcp:ACTIVE_KEYS";
+       public final static String CANCEL_KEYS = "argeo-rcp:CANCEL_KEYS";
+
+       public final static int HYPERLINK = 0;
+
+       private static Locale locale = Locale.getDefault();
+       private static RcpClient client = new RcpClient();
+       private static ResourceManager resourceManager = new RcpResourceManager();
+       static {
+
+       }
+
+       public static Locale getLocale() {
+               return locale;
+       }
+
+       public static HttpServletRequest getRequest() {
+               return null;
+       }
+
+       public static ResourceManager getResourceManager() {
+               return resourceManager;
+       }
+
+       public static Client getClient() {
+               return client;
+       }
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/SingletonUtil.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/SingletonUtil.java
new file mode 100644 (file)
index 0000000..6e30aa6
--- /dev/null
@@ -0,0 +1,7 @@
+package org.eclipse.rap.rwt;
+
+public class SingletonUtil {
+       public static <T> T getSessionInstance(Class<T> clss) {
+               return null;
+       }
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/AbstractEntryPoint.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/AbstractEntryPoint.java
new file mode 100644 (file)
index 0000000..980a818
--- /dev/null
@@ -0,0 +1,43 @@
+package org.eclipse.rap.rwt.application;
+
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+public abstract class AbstractEntryPoint implements EntryPoint {
+       private Display display;
+       private Shell shell;
+
+       protected Shell createShell(Display display) {
+               return new Shell(display);
+       }
+
+       protected void createContents(Composite parent) {
+
+       }
+
+       public int createUI() {
+               display = new Display();
+               shell = createShell(display);
+               shell.setLayout(new GridLayout(1, false));
+               createContents(shell);
+               if (shell.getMaximized()) {
+                       shell.layout();
+               } else {
+                       shell.pack();
+               }
+               shell.open();
+               while (!shell.isDisposed()) {
+                       if (!display.readAndDispatch()) {
+                               display.sleep();
+                       }
+               }
+               display.dispose();
+               return 0;
+       }
+
+       protected Shell getShell() {
+               return shell;
+       }
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/Application.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/Application.java
new file mode 100644 (file)
index 0000000..6cb5f29
--- /dev/null
@@ -0,0 +1,27 @@
+package org.eclipse.rap.rwt.application;
+
+import java.util.Map;
+
+import org.eclipse.rap.rwt.service.ResourceLoader;
+
+public interface Application {
+       public static enum OperationMode {
+               JEE_COMPATIBILITY, SWT_COMPATIBILITY,
+       }
+
+       void setOperationMode(OperationMode operationMode);
+
+       void addResource(String name, ResourceLoader resourceLoader);
+
+       void setExceptionHandler(ExceptionHandler exceptionHandler);
+
+       void addEntryPoint(String path, EntryPointFactory entryPointFactory,
+                       Map<String, String> properties);
+
+       void addEntryPoint(String path, Class<? extends EntryPoint> entryPoint,
+                       Map<String, String> properties);
+
+       void addStyleSheet(String themeId, String styleSheetLocation,
+                       ResourceLoader resourceLoader);
+
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/ApplicationConfiguration.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/ApplicationConfiguration.java
new file mode 100644 (file)
index 0000000..961ad70
--- /dev/null
@@ -0,0 +1,5 @@
+package org.eclipse.rap.rwt.application;
+
+public interface ApplicationConfiguration {
+       void configure(Application application);
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/EntryPoint.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/EntryPoint.java
new file mode 100644 (file)
index 0000000..c0d559a
--- /dev/null
@@ -0,0 +1,5 @@
+package org.eclipse.rap.rwt.application;
+
+public interface EntryPoint {
+       int createUI();
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/EntryPointFactory.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/EntryPointFactory.java
new file mode 100644 (file)
index 0000000..d5b24d8
--- /dev/null
@@ -0,0 +1,5 @@
+package org.eclipse.rap.rwt.application;
+
+public interface EntryPointFactory {
+       public EntryPoint create();
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/ExceptionHandler.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/application/ExceptionHandler.java
new file mode 100644 (file)
index 0000000..13daf21
--- /dev/null
@@ -0,0 +1,5 @@
+package org.eclipse.rap.rwt.application;
+
+public interface ExceptionHandler {
+       public void handleException(Throwable throwable);
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/Client.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/Client.java
new file mode 100644 (file)
index 0000000..934feae
--- /dev/null
@@ -0,0 +1,18 @@
+package org.eclipse.rap.rwt.client;
+
+import java.io.Serializable;
+
+import org.eclipse.rap.rwt.client.service.ClientService;
+
+public interface Client extends Serializable {
+
+  /**
+   * Returns this client's implementation of a given service, if available.
+   *
+   * @param type the type of the requested service, must be a subtype of ClientService
+   * @return the requested service if provided by this client, otherwise <code>null</code>
+   * @see ClientService
+   */
+  <T extends ClientService> T getService( Class<T> type );
+
+}
\ No newline at end of file
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/WebClient.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/WebClient.java
new file mode 100644 (file)
index 0000000..1f19bdd
--- /dev/null
@@ -0,0 +1,10 @@
+package org.eclipse.rap.rwt.client;
+
+public interface WebClient {
+       public final static String FAVICON = "rcp:FAVICON";
+       public final static String PAGE_TITLE = "rcp:PAGE_TITLE";
+       public final static String BODY_HTML = "rcp:BODY_HTML";
+       public final static String THEME_ID = "rcp:THEME_ID";
+       public final static String HEAD_HTML = "rcp:HEAD_HTML";
+       public final static String PAGE_OVERFLOW = "rcp:PAGE_OVERFLOW";
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/BrowserNavigation.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/BrowserNavigation.java
new file mode 100644 (file)
index 0000000..ffba4e4
--- /dev/null
@@ -0,0 +1,7 @@
+package org.eclipse.rap.rwt.client.service;
+
+public interface BrowserNavigation extends ClientService {
+       void pushState(String state, String title);
+
+       void addBrowserNavigationListener(BrowserNavigationListener listener);
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/BrowserNavigationEvent.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/BrowserNavigationEvent.java
new file mode 100644 (file)
index 0000000..3e1b3eb
--- /dev/null
@@ -0,0 +1,10 @@
+package org.eclipse.rap.rwt.client.service;
+
+public class BrowserNavigationEvent {
+       private String state;
+
+       public String getState() {
+               return state;
+       }
+
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/BrowserNavigationListener.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/BrowserNavigationListener.java
new file mode 100644 (file)
index 0000000..8319c03
--- /dev/null
@@ -0,0 +1,5 @@
+package org.eclipse.rap.rwt.client.service;
+
+public interface BrowserNavigationListener {
+       public void navigated(BrowserNavigationEvent event);
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/ClientService.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/ClientService.java
new file mode 100644 (file)
index 0000000..9f479d1
--- /dev/null
@@ -0,0 +1,6 @@
+package org.eclipse.rap.rwt.client.service;
+
+import java.io.Serializable;
+
+public interface ClientService extends Serializable {
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/JavaScriptExecutor.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/JavaScriptExecutor.java
new file mode 100644 (file)
index 0000000..6c44c72
--- /dev/null
@@ -0,0 +1,5 @@
+package org.eclipse.rap.rwt.client.service;
+
+public interface JavaScriptExecutor extends ClientService {
+       public void execute( String code );
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/UrlLauncher.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/client/service/UrlLauncher.java
new file mode 100644 (file)
index 0000000..9dae811
--- /dev/null
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Copyright (c) 2012 EclipseSource and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *    EclipseSource - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.rap.rwt.client.service;
+
+/**
+ * The UrlLauncher service allows loading an URL in an external window, application or save dialog.
+ *
+ * @since 2.0
+ * @noimplement This interface is not intended to be implemented by clients.
+ */
+public interface UrlLauncher extends ClientService {
+
+  /**
+   * Opens the given URL.
+   *
+   * Any HTTP URL or relative URL will be opened in a new window.
+   * Modern browser may block any attempt to open new windows, but will usually prompt the user to
+   * accept or ignore. Even if accepted, the decision may be applied to only this attempt, or only
+   * to future attempts. It could also trigger a document reload, causing a session restart.
+   *
+   * Non-HTTP URLs like "mailto" will not create a new browser window, but require the client
+   * to have a matching protocol handler registered.
+   *
+   * @param url the URL to open
+   */
+  void openURL( String url );
+
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/service/ResourceLoader.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/service/ResourceLoader.java
new file mode 100644 (file)
index 0000000..7e7116c
--- /dev/null
@@ -0,0 +1,9 @@
+package org.eclipse.rap.rwt.service;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public interface ResourceLoader {
+       public InputStream getResourceAsStream(String resourceName)
+                       throws IOException;
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/service/ResourceManager.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/service/ResourceManager.java
new file mode 100644 (file)
index 0000000..c3379ea
--- /dev/null
@@ -0,0 +1,15 @@
+package org.eclipse.rap.rwt.service;
+
+import java.io.InputStream;
+
+public interface ResourceManager {
+       public void register(String name, InputStream in);
+
+       boolean unregister(String name);
+
+       public InputStream getRegisteredContent(String name);
+
+       public String getLocation(String name);
+
+       public boolean isRegistered(String name);
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/service/ServerPushSession.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/service/ServerPushSession.java
new file mode 100644 (file)
index 0000000..bed194f
--- /dev/null
@@ -0,0 +1,12 @@
+package org.eclipse.rap.rwt.service;
+
+/** Mock, does nothing as this is irrelevant for RCP. */
+public class ServerPushSession {
+       public void start() {
+
+       }
+
+       public void stop() {
+
+       }
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/widgets/DropDown.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/widgets/DropDown.java
new file mode 100644 (file)
index 0000000..b2a2005
--- /dev/null
@@ -0,0 +1,33 @@
+package org.eclipse.rap.rwt.widgets;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Widget;
+
+public class DropDown {
+       private boolean visible=false;
+
+       public DropDown(Widget parent, int style) {
+               // FIXME implement a shell
+       }
+
+       public DropDown(Widget parent) {
+               this(parent, SWT.NONE);
+       }
+
+       public void setVisible(boolean visible) {
+               this.visible = visible;
+       }
+
+       public boolean isVisible() {
+               return visible;
+       }
+       
+       public void setItems( String[] items ) {
+               
+       }
+       
+       public void setSelectionIndex( int selection ) {
+               
+       }
+       
+}
diff --git a/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/widgets/FileUpload.java b/rcp/org.argeo.eclipse.ui.rcp/src/org/eclipse/rap/rwt/widgets/FileUpload.java
new file mode 100644 (file)
index 0000000..e955516
--- /dev/null
@@ -0,0 +1,28 @@
+package org.eclipse.rap.rwt.widgets;
+
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+
+public class FileUpload extends Composite {
+
+       public FileUpload(Composite parent, int style) {
+               super(parent, style);
+       }
+
+       public void addSelectionListener(SelectionListener listener) {
+
+       }
+
+       public void submit(String url) {
+
+       }
+       
+        public void setImage( Image image ) {
+                
+        }
+        
+        public void setText(String text){
+                
+        }
+}
diff --git a/rcp/pom.xml b/rcp/pom.xml
new file mode 100644 (file)
index 0000000..9108d34
--- /dev/null
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.argeo.commons</groupId>
+               <artifactId>argeo-commons</artifactId>
+               <version>2.1.76-SNAPSHOT</version>
+               <relativePath>..</relativePath>
+       </parent>
+       <groupId>org.argeo.commons.rcp</groupId>
+       <artifactId>argeo-rcp</artifactId>
+       <name>Argeo RCP</name>
+       <packaging>pom</packaging>
+       <modules>
+               <module>org.argeo.eclipse.ui.rcp</module>
+               <module>org.argeo.cms.e4.rcp</module>
+               <module>dep</module>
+               <!-- <module>demo</module> -->
+       </modules>
+       <dependencies>
+               <dependency>
+                       <groupId>org.argeo.tp</groupId>
+                       <artifactId>argeo-tp</artifactId>
+                       <version>${version.argeo-tp}</version>
+                       <scope>provided</scope>
+                       <exclusions>
+                               <!-- Equinox base -->
+                               <!-- <exclusion> -->
+                               <!-- <groupId>org.argeo.tp.equinox</groupId> -->
+                               <!-- <artifactId>org.eclipse.osgi</artifactId> -->
+                               <!-- </exclusion> -->
+                               <!-- <exclusion> -->
+                               <!-- <groupId>org.argeo.tp.equinox</groupId> -->
+                               <!-- <artifactId>org.eclipse.osgi.services</artifactId> -->
+                               <!-- </exclusion> -->
+
+                               <!-- RAP UI -->
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.rap.ui.forms</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.rap.rwt</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.rap.jface</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.rap.ui</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.rap.ui.views</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.rap.ui.workbench</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.rap.rwt.osgi</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.rap.jface.databinding</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.core.jobs</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.core.expressions</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.core.databinding.observable</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.help</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.core.databinding</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.core.databinding.beans</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.core.runtime</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.core.databinding.property</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>com.ibm.icu.base</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.core.contenttype</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.core.commands</artifactId>
+                               </exclusion>
+
+                               <!-- Addons -->
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.rap.filedialog</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rap.platform</groupId>
+                                       <artifactId>org.eclipse.rap.fileupload</artifactId>
+                               </exclusion>
+                       </exclusions>
+               </dependency>
+               <dependency>
+                       <groupId>org.argeo.tp</groupId>
+                       <artifactId>argeo-tp-rcp-e4</artifactId>
+                       <version>${version.argeo-tp}</version>
+                       <scope>provided</scope>
+                       <exclusions>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rcp.platform</groupId>
+                                       <artifactId>org.eclipse.osgi</artifactId>
+                               </exclusion>
+                               <exclusion>
+                                       <groupId>org.argeo.tp.rcp.platform</groupId>
+                                       <artifactId>org.eclipse.osgi.services</artifactId>
+                               </exclusion>
+                       </exclusions>
+               </dependency>
+               <!-- <dependency> -->
+               <!-- <groupId>org.argeo.tp.eclipse.ide</groupId> -->
+               <!-- <artifactId>org.eclipse.ui.forms</artifactId> -->
+               <!-- <version>3.7.101.v20170815-1446</version> -->
+               <!-- <scope>provided</scope> -->
+               <!-- </dependency> -->
+       </dependencies>
+       <dependencyManagement>
+               <dependencies>
+                       <dependency>
+                               <groupId>org.argeo.tp</groupId>
+                               <artifactId>argeo-tp-rcp-e4</artifactId>
+                               <version>${version.argeo-tp}</version>
+                               <type>pom</type>
+                               <scope>import</scope>
+                       </dependency>
+               </dependencies>
+       </dependencyManagement>
+</project>