From bce03099b0d2f1758e7a3d74fba339d0200924d5 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Sat, 15 Oct 2022 11:00:51 +0200 Subject: [PATCH] Code move and initial build --- .gitignore | 4 + .gitmodules | 4 + Makefile | 28 + branch.mk | 1 + cnf/unstable.bnd | 6 + configure | 7 + org.argeo.cms.jcr.ui/.classpath | 7 + org.argeo.cms.jcr.ui/.project | 28 + org.argeo.cms.jcr.ui/META-INF/.gitignore | 1 + org.argeo.cms.jcr.ui/bnd.bnd | 23 + org.argeo.cms.jcr.ui/build.properties | 5 + org.argeo.cms.jcr.ui/icons/loading.gif | Bin 0 -> 3208 bytes .../icons/noPic-goldenRatio-640px.png | Bin 0 -> 6246 bytes .../icons/noPic-square-640px.png | Bin 0 -> 4314 bytes .../src/org/argeo/cms/ui/CmsUiConstants.java | 23 + .../src/org/argeo/cms/ui/CmsUiProvider.java | 51 + .../org/argeo/cms/ui/LifeCycleUiProvider.java | 11 + .../ui/eclipse/forms/AbstractFormPart.java | 108 + .../cms/ui/eclipse/forms/FormColors.java | 730 +++++++ .../argeo/cms/ui/eclipse/forms/FormFonts.java | 122 ++ .../cms/ui/eclipse/forms/FormToolkit.java | 913 +++++++++ .../argeo/cms/ui/eclipse/forms/FormUtil.java | 522 +++++ .../cms/ui/eclipse/forms/IFormColors.java | 102 + .../argeo/cms/ui/eclipse/forms/IFormPart.java | 108 + .../cms/ui/eclipse/forms/IManagedForm.java | 175 ++ .../eclipse/forms/IPartSelectionListener.java | 23 + .../cms/ui/eclipse/forms/ManagedForm.java | 323 +++ .../ui/eclipse/forms/editor/FormEditor.java | 85 + .../cms/ui/eclipse/forms/editor/FormPage.java | 276 +++ .../ui/eclipse/forms/editor/IFormPage.java | 119 ++ .../org/argeo/cms/ui/forms/EditableLink.java | 75 + .../ui/forms/EditableMultiStringProperty.java | 261 +++ .../cms/ui/forms/EditablePropertyDate.java | 298 +++ .../cms/ui/forms/EditablePropertyString.java | 80 + .../org/argeo/cms/ui/forms/FormConstants.java | 7 + .../argeo/cms/ui/forms/FormEditorHeader.java | 114 ++ .../argeo/cms/ui/forms/FormPageViewer.java | 608 ++++++ .../src/org/argeo/cms/ui/forms/FormStyle.java | 29 + .../src/org/argeo/cms/ui/forms/FormUtils.java | 196 ++ .../cms/ui/forms/MarkupValidatorCopy.java | 169 ++ .../org/argeo/cms/ui/forms/package-info.java | 2 + .../src/org/argeo/cms/ui/fs/CmsFsBrowser.java | 524 +++++ .../src/org/argeo/cms/ui/fs/FileDrop.java | 37 + .../org/argeo/cms/ui/fs/FsContextMenu.java | 383 ++++ .../src/org/argeo/cms/ui/fs/FsStyles.java | 8 + .../src/org/argeo/cms/ui/fs/package-info.java | 2 + .../org/argeo/cms/ui/internal/Activator.java | 37 + .../cms/ui/internal/JcrContentProvider.java | 81 + .../ui/internal/JcrFileUploadReceiver.java | 74 + .../cms/ui/internal/SimpleEditableImage.java | 74 + .../cms/ui/jcr/DefaultRepositoryRegister.java | 75 + .../FullVersioningTreeContentProvider.java | 98 + .../org/argeo/cms/ui/jcr/JcrBrowserUtils.java | 68 + .../argeo/cms/ui/jcr/JcrDClickListener.java | 60 + .../src/org/argeo/cms/ui/jcr/JcrImages.java | 24 + .../cms/ui/jcr/JcrTreeContentProvider.java | 82 + .../argeo/cms/ui/jcr/NodeContentProvider.java | 175 ++ .../argeo/cms/ui/jcr/NodeLabelProvider.java | 113 ++ .../cms/ui/jcr/OsgiRepositoryRegister.java | 52 + .../cms/ui/jcr/PropertiesContentProvider.java | 42 + .../cms/ui/jcr/PropertyLabelProvider.java | 101 + .../argeo/cms/ui/jcr/RepositoryRegister.java | 16 + .../cms/ui/jcr/VersionLabelProvider.java | 33 + .../jcr/model/MaintainedRepositoryElem.java | 21 + .../ui/jcr/model/RemoteRepositoryElem.java | 76 + .../cms/ui/jcr/model/RepositoriesElem.java | 112 ++ .../cms/ui/jcr/model/RepositoryElem.java | 98 + .../cms/ui/jcr/model/SingleJcrNodeElem.java | 84 + .../argeo/cms/ui/jcr/model/WorkspaceElem.java | 117 ++ .../argeo/cms/ui/jcr/model/package-info.java | 2 + .../org/argeo/cms/ui/jcr/package-info.java | 2 + .../src/org/argeo/cms/ui/package-info.java | 2 + .../src/org/argeo/cms/ui/util/CmsLink.java | 282 +++ .../src/org/argeo/cms/ui/util/CmsPane.java | 49 + .../src/org/argeo/cms/ui/util/CmsUiUtils.java | 192 ++ .../cms/ui/util/DefaultImageManager.java | 133 ++ .../src/org/argeo/cms/ui/util/MenuLink.java | 22 + .../argeo/cms/ui/util/SimpleCmsHeader.java | 97 + .../argeo/cms/ui/util/SimpleDynamicPages.java | 118 ++ .../argeo/cms/ui/util/SimpleStaticPage.java | 32 + .../org/argeo/cms/ui/util/SimpleStyle.java | 8 + .../cms/ui/util/StyleSheetResourceLoader.java | 71 + .../cms/ui/util/SystemNotifications.java | 129 ++ .../src/org/argeo/cms/ui/util/UserMenu.java | 56 + .../org/argeo/cms/ui/util/UserMenuLink.java | 84 + .../org/argeo/cms/ui/util/VerticalMenu.java | 44 + .../org/argeo/cms/ui/util/package-info.java | 2 + .../cms/ui/viewers/AbstractPageViewer.java | 351 ++++ .../org/argeo/cms/ui/viewers/ItemPart.java | 9 + .../cms/ui/viewers/JcrVersionCmsEditable.java | 94 + .../org/argeo/cms/ui/viewers/NodePart.java | 8 + .../argeo/cms/ui/viewers/PropertyPart.java | 8 + .../src/org/argeo/cms/ui/viewers/Section.java | 166 ++ .../org/argeo/cms/ui/viewers/SectionPart.java | 10 + .../argeo/cms/ui/viewers/package-info.java | 2 + .../argeo/cms/ui/widgets/EditableImage.java | 112 ++ .../argeo/cms/ui/widgets/EditableText.java | 145 ++ .../src/org/argeo/cms/ui/widgets/Img.java | 155 ++ .../argeo/cms/ui/widgets/JcrComposite.java | 213 ++ .../argeo/cms/ui/widgets/StyledControl.java | 153 ++ .../org/argeo/cms/ui/widgets/TextStyles.java | 37 + .../argeo/cms/ui/widgets/package-info.java | 2 + .../ui/jcr/AbstractNodeContentProvider.java | 138 ++ .../eclipse/ui/jcr/AsyncUiEventListener.java | 83 + .../ui/jcr/DefaultNodeLabelProvider.java | 82 + .../org/argeo/eclipse/ui/jcr/JcrUiUtils.java | 149 ++ .../ui/jcr/NodeColumnLabelProvider.java | 123 ++ .../org/argeo/eclipse/ui/jcr/NodeElement.java | 8 + .../eclipse/ui/jcr/NodeElementComparer.java | 36 + .../argeo/eclipse/ui/jcr/NodesWrapper.java | 71 + .../ui/jcr/QueryTableContentProvider.java | 35 + .../ui/jcr/RowColumnLabelProvider.java | 111 + .../ui/jcr/SimpleNodeContentProvider.java | 59 + .../ui/jcr/VersionColumnLabelProvider.java | 80 + .../ui/jcr/VersionHistoryContentProvider.java | 27 + .../org/argeo/eclipse/ui/jcr/WrappedNode.java | 41 + .../ui/jcr/lists/JcrColumnDefinition.java | 116 ++ .../ui/jcr/lists/NodeViewerComparator.java | 190 ++ .../ui/jcr/lists/RowViewerComparator.java | 62 + .../jcr/lists/SimpleJcrNodeLabelProvider.java | 120 ++ .../jcr/lists/SimpleJcrRowLabelProvider.java | 47 + .../eclipse/ui/jcr/lists/package-info.java | 2 + .../argeo/eclipse/ui/jcr/package-info.java | 2 + .../eclipse/ui/jcr/util/JcrFileProvider.java | 129 ++ .../ui/jcr/util/JcrItemsComparator.java | 21 + .../ui/jcr/util/NodeViewerComparer.java | 36 + .../jcr/util/SingleSessionFileProvider.java | 98 + .../eclipse/ui/jcr/util/package-info.java | 2 + org.argeo.cms.jcr/.classpath | 11 + org.argeo.cms.jcr/.project | 33 + .../.settings/org.eclipse.jdt.core.prefs | 101 + .../OSGI-INF/dataServletContext.xml | 9 + org.argeo.cms.jcr/OSGI-INF/filesServlet.xml | 12 + .../OSGI-INF/filesServletContext.xml | 9 + .../OSGI-INF/jcrContentProvider.xml | 10 + org.argeo.cms.jcr/OSGI-INF/jcrDeployment.xml | 4 + org.argeo.cms.jcr/OSGI-INF/jcrFsProvider.xml | 10 + .../OSGI-INF/jcrRepositoryFactory.xml | 8 + .../OSGI-INF/jcrServletContext.xml | 9 + .../OSGI-INF/repositoryContextsFactory.xml | 6 + org.argeo.cms.jcr/bnd.bnd | 40 + org.argeo.cms.jcr/build.properties | 8 + .../src/org/argeo/cms/fs/CmsFsUtils.java | 88 + .../CustomRepositoryConfigurationParser.java | 58 + .../cms/internal/jcr/JackrabbitType.java | 21 + .../cms/internal/jcr/LocalFsDataStore.java | 68 + .../org/argeo/cms/internal/jcr/RepoConf.java | 55 + .../cms/internal/jcr/RepositoryBuilder.java | 225 +++ .../argeo/cms/internal/jcr/repository-h2.xml | 86 + .../internal/jcr/repository-h2_postgresql.xml | 86 + .../cms/internal/jcr/repository-localfs.xml | 60 + .../cms/internal/jcr/repository-memory.xml | 59 + .../internal/jcr/repository-postgresql.xml | 79 + .../jcr/repository-postgresql_cluster.xml | 87 + .../jcr/repository-postgresql_cluster_ds.xml | 103 + .../internal/jcr/repository-postgresql_ds.xml | 82 + .../src/org/argeo/cms/jcr/CmsJcrUtils.java | 278 +++ .../src/org/argeo/cms/jcr/acr/JcrContent.java | 387 ++++ .../argeo/cms/jcr/acr/JcrContentProvider.java | 122 ++ .../argeo/cms/jcr/acr/JcrContentUtils.java | 261 +++ .../argeo/cms/jcr/acr/JcrSessionAdapter.java | 91 + .../jcr/acr/JcrSessionNamespaceContext.java | 46 + .../src/org/argeo/cms/jcr/argeo.cnd | 34 + .../src/org/argeo/cms/jcr/dn.cnd | 10 + .../cms/jcr/internal/CmsJcrDeployment.java | 475 +++++ .../cms/jcr/internal/CmsJcrFsProvider.java | 133 ++ .../org/argeo/cms/jcr/internal/CmsPaths.java | 19 + .../cms/jcr/internal/CmsWorkspaceIndexer.java | 342 ++++ .../argeo/cms/jcr/internal/DataModels.java | 190 ++ .../argeo/cms/jcr/internal/EgoRepository.java | 264 +++ .../internal/JackrabbitLocalRepository.java | 71 + .../argeo/cms/jcr/internal/JcrKeyring.java | 397 ++++ .../jcr/internal/JcrRepositoryFactory.java | 191 ++ .../cms/jcr/internal/KernelConstants.java | 53 + .../argeo/cms/jcr/internal/KernelUtils.java | 262 +++ .../cms/jcr/internal/LocalRepository.java | 23 + .../argeo/cms/jcr/internal/NodeKeyRing.java | 19 + .../internal/RepositoryContextsFactory.java | 191 ++ .../cms/jcr/internal/StatisticsThread.java | 123 ++ .../jcr/internal/osgi/CmsJcrActivator.java | 91 + .../internal/servlet/CmsRemotingServlet.java | 44 + .../internal/servlet/CmsSessionProvider.java | 175 ++ .../internal/servlet/CmsWebDavServlet.java | 37 + .../internal/servlet/DataServletContext.java | 8 + .../jcr/internal/servlet/JcrHttpUtils.java | 73 + .../jcr/internal/servlet/JcrReadServlet.java | 319 +++ .../internal/servlet/JcrServletContext.java | 8 + .../jcr/internal/servlet/JcrWriteServlet.java | 92 + .../cms/jcr/internal/servlet/LinkServlet.java | 258 +++ .../internal/servlet/protectedHandlers.xml | 5 + .../jcr/internal/servlet/webdav-config.xml | 207 ++ .../src/org/argeo/cms/jcr/ldap.cnd | 1 + .../src/org/argeo/cms/jcr/node.cnd | 9 + .../cms/jcr/tabular/CsvTabularWriter.java | 23 + .../jcr/tabular/JcrTabularRowIterator.java | 170 ++ .../cms/jcr/tabular/JcrTabularWriter.java | 82 + .../argeo/cms/jcr/tabular/package-info.java | 2 + .../JackrabbitAdminLoginModule.java | 48 + .../JackrabbitDataModelMigration.java | 172 ++ .../client/ClientDavexRepositoryFactory.java | 26 + .../client/ClientDavexRepositoryService.java | 51 + .../ClientDavexRepositoryServiceFactory.java | 84 + .../jackrabbit/client/JackrabbitClient.java | 127 ++ .../client/NonSerialBasicAuthCache.java | 41 + .../fs/AbstractJackrabbitFsProvider.java | 7 + .../argeo/jackrabbit/fs/DavexFsProvider.java | 149 ++ .../fs/JackrabbitMemoryFsProvider.java | 87 + .../src/org/argeo/jackrabbit/fs/fs-memory.xml | 57 + .../org/argeo/jackrabbit/fs/package-info.java | 2 + .../org/argeo/jackrabbit/package-info.java | 2 + .../org/argeo/jackrabbit/repository-h2.xml | 82 + .../argeo/jackrabbit/repository-localfs.xml | 60 + .../argeo/jackrabbit/repository-memory.xml | 55 + .../jackrabbit/repository-postgresql-ds.xml | 82 + .../jackrabbit/repository-postgresql.xml | 79 + .../security/JackrabbitSecurityUtils.java | 79 + .../jackrabbit/security/package-info.java | 2 + org.argeo.cms.jcr/src/org/argeo/jcr/Bin.java | 60 + .../org/argeo/jcr/CollectionNodeIterator.java | 61 + .../src/org/argeo/jcr/DefaultJcrListener.java | 77 + org.argeo.cms.jcr/src/org/argeo/jcr/Jcr.java | 993 +++++++++ .../src/org/argeo/jcr/JcrAuthorizations.java | 207 ++ .../src/org/argeo/jcr/JcrCallback.java | 15 + .../src/org/argeo/jcr/JcrException.java | 22 + .../src/org/argeo/jcr/JcrMonitor.java | 87 + .../org/argeo/jcr/JcrRepositoryWrapper.java | 244 +++ .../org/argeo/jcr/JcrUrlStreamHandler.java | 70 + .../src/org/argeo/jcr/JcrUtils.java | 1786 +++++++++++++++++ .../src/org/argeo/jcr/JcrxApi.java | 190 ++ .../src/org/argeo/jcr/JcrxName.java | 7 + .../src/org/argeo/jcr/JcrxType.java | 17 + .../src/org/argeo/jcr/PropertyDiff.java | 57 + .../src/org/argeo/jcr/SimplePrincipal.java | 43 + .../jcr/ThreadBoundJcrSessionFactory.java | 279 +++ .../src/org/argeo/jcr/VersionDiff.java | 38 + .../src/org/argeo/jcr/fs/BinaryChannel.java | 190 ++ .../argeo/jcr/fs/JcrBasicfileAttributes.java | 138 ++ .../src/org/argeo/jcr/fs/JcrFileSystem.java | 252 +++ .../argeo/jcr/fs/JcrFileSystemProvider.java | 337 ++++ .../src/org/argeo/jcr/fs/JcrFsException.java | 15 + .../src/org/argeo/jcr/fs/JcrPath.java | 384 ++++ .../org/argeo/jcr/fs/NodeDirectoryStream.java | 77 + .../org/argeo/jcr/fs/NodeFileAttributes.java | 9 + .../src/org/argeo/jcr/fs/Text.java | 877 ++++++++ .../org/argeo/jcr/fs/WorkspaceFileStore.java | 192 ++ .../src/org/argeo/jcr/fs/package-info.java | 2 + org.argeo.cms.jcr/src/org/argeo/jcr/jcrx.cnd | 16 + .../src/org/argeo/jcr/package-info.java | 2 + .../org/argeo/jcr/proxy/AbstractUrlProxy.java | 153 ++ .../org/argeo/jcr/proxy/ResourceProxy.java | 16 + .../argeo/jcr/proxy/ResourceProxyServlet.java | 115 ++ .../src/org/argeo/jcr/proxy/package-info.java | 2 + .../src/org/argeo/jcr/xml/JcrXmlUtils.java | 186 ++ .../src/org/argeo/jcr/xml/removePrefixes.xsl | 19 + .../AbstractMaintenanceService.java | 232 +++ .../maintenance/SimpleRoleRegistration.java | 86 + .../backup/BackupContentHandler.java | 257 +++ .../maintenance/backup/LogicalBackup.java | 448 +++++ .../maintenance/backup/LogicalRestore.java | 85 + .../maintenance/backup/package-info.java | 2 + .../argeo/maintenance/internal/Activator.java | 27 + .../org/argeo/maintenance/package-info.java | 2 + .../ArgeoAccessControlProvider.java | 30 + .../jackrabbit/ArgeoAccessManager.java | 35 + .../security/jackrabbit/ArgeoAuthContext.java | 37 + .../jackrabbit/ArgeoSecurityManager.java | 159 ++ .../SystemJackrabbitLoginModule.java | 67 + .../security/jackrabbit/package-info.java | 2 + sdk/argeo-build | 1 + 269 files changed, 29979 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Makefile create mode 100644 branch.mk create mode 100644 cnf/unstable.bnd create mode 100755 configure create mode 100644 org.argeo.cms.jcr.ui/.classpath create mode 100644 org.argeo.cms.jcr.ui/.project create mode 100644 org.argeo.cms.jcr.ui/META-INF/.gitignore create mode 100644 org.argeo.cms.jcr.ui/bnd.bnd create mode 100644 org.argeo.cms.jcr.ui/build.properties create mode 100644 org.argeo.cms.jcr.ui/icons/loading.gif create mode 100644 org.argeo.cms.jcr.ui/icons/noPic-goldenRatio-640px.png create mode 100644 org.argeo.cms.jcr.ui/icons/noPic-square-640px.png create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/CmsUiConstants.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/CmsUiProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/LifeCycleUiProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormColors.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditableLink.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditableMultiStringProperty.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditablePropertyDate.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditablePropertyString.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormConstants.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormEditorHeader.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormPageViewer.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormStyle.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormUtils.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/MarkupValidatorCopy.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/package-info.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/CmsFsBrowser.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/FileDrop.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/FsContextMenu.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/FsStyles.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/package-info.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/Activator.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/JcrContentProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/JcrFileUploadReceiver.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/SimpleEditableImage.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrDClickListener.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrImages.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/NodeContentProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/RepositoryRegister.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/package-info.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/package-info.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/package-info.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsLink.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsPane.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsUiUtils.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/DefaultImageManager.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/MenuLink.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleCmsHeader.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleDynamicPages.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleStaticPage.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleStyle.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/StyleSheetResourceLoader.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SystemNotifications.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/UserMenu.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/UserMenuLink.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/VerticalMenu.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/package-info.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/AbstractPageViewer.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/ItemPart.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/JcrVersionCmsEditable.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/NodePart.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/PropertyPart.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/Section.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/SectionPart.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/package-info.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/EditableImage.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/EditableText.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/Img.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/JcrComposite.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/StyledControl.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/TextStyles.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/package-info.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/AbstractNodeContentProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/DefaultNodeLabelProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/JcrUiUtils.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeColumnLabelProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeElement.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeElementComparer.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodesWrapper.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/QueryTableContentProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/RowColumnLabelProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/SimpleNodeContentProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/VersionColumnLabelProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/VersionHistoryContentProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/WrappedNode.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/JcrColumnDefinition.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/NodeViewerComparator.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/RowViewerComparator.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrNodeLabelProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrRowLabelProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/package-info.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/package-info.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/JcrFileProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/JcrItemsComparator.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/NodeViewerComparer.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/SingleSessionFileProvider.java create mode 100644 org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/package-info.java create mode 100644 org.argeo.cms.jcr/.classpath create mode 100644 org.argeo.cms.jcr/.project create mode 100644 org.argeo.cms.jcr/.settings/org.eclipse.jdt.core.prefs create mode 100644 org.argeo.cms.jcr/OSGI-INF/dataServletContext.xml create mode 100644 org.argeo.cms.jcr/OSGI-INF/filesServlet.xml create mode 100644 org.argeo.cms.jcr/OSGI-INF/filesServletContext.xml create mode 100644 org.argeo.cms.jcr/OSGI-INF/jcrContentProvider.xml create mode 100644 org.argeo.cms.jcr/OSGI-INF/jcrDeployment.xml create mode 100644 org.argeo.cms.jcr/OSGI-INF/jcrFsProvider.xml create mode 100644 org.argeo.cms.jcr/OSGI-INF/jcrRepositoryFactory.xml create mode 100644 org.argeo.cms.jcr/OSGI-INF/jcrServletContext.xml create mode 100644 org.argeo.cms.jcr/OSGI-INF/repositoryContextsFactory.xml create mode 100644 org.argeo.cms.jcr/bnd.bnd create mode 100644 org.argeo.cms.jcr/build.properties create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/fs/CmsFsUtils.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/CustomRepositoryConfigurationParser.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/JackrabbitType.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/LocalFsDataStore.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/RepoConf.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/RepositoryBuilder.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-h2.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-h2_postgresql.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-localfs.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-memory.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster_ds.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_ds.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/CmsJcrUtils.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentUtils.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionAdapter.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionNamespaceContext.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/argeo.cnd create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/dn.cnd create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrDeployment.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrFsProvider.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsPaths.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsWorkspaceIndexer.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/DataModels.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/EgoRepository.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JackrabbitLocalRepository.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JcrKeyring.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JcrRepositoryFactory.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/KernelConstants.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/KernelUtils.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/LocalRepository.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/NodeKeyRing.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/RepositoryContextsFactory.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/StatisticsThread.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/osgi/CmsJcrActivator.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsRemotingServlet.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsSessionProvider.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsWebDavServlet.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/DataServletContext.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrHttpUtils.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrReadServlet.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrServletContext.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrWriteServlet.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/LinkServlet.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/protectedHandlers.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/webdav-config.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/ldap.cnd create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/node.cnd create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/CsvTabularWriter.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/JcrTabularRowIterator.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/JcrTabularWriter.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/package-info.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/JackrabbitClient.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/package-info.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/package-info.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-h2.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-localfs.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-memory.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/package-info.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/Bin.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/CollectionNodeIterator.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/DefaultJcrListener.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/Jcr.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/JcrAuthorizations.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/JcrCallback.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/JcrException.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/JcrMonitor.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/JcrUtils.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/JcrxApi.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/JcrxName.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/JcrxType.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/PropertyDiff.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/SimplePrincipal.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/VersionDiff.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/BinaryChannel.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFsException.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrPath.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/Text.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/fs/package-info.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/jcrx.cnd create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/package-info.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/proxy/package-info.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl create mode 100644 org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/maintenance/SimpleRoleRegistration.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/maintenance/backup/BackupContentHandler.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalBackup.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalRestore.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/maintenance/backup/package-info.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/maintenance/internal/Activator.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/maintenance/package-info.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAccessManager.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAuthContext.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/SystemJackrabbitLoginModule.java create mode 100644 org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/package-info.java create mode 160000 sdk/argeo-build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11caf47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +**/bin/ +**/MANIFEST.MF +/sdk.mk +/output/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7802444 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "argeo-build"] + path = sdk/argeo-build + url = http://git.argeo.org/cc0/argeo-build.git + branch = unstable diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7223d31 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +include sdk.mk + +all: osgi + +A2_CATEGORY = org.argeo.cms.jcr + +BUNDLES = \ +org.argeo.cms.jcr \ +org.argeo.cms.jcr.ui \ + +DEP_CATEGORIES = \ +org.argeo.tp \ +org.argeo.tp.apache \ +org.argeo.tp.jetty \ +osgi/api/org.argeo.tp.osgi \ +osgi/equinox/org.argeo.tp.eclipse \ +swt/rap/org.argeo.tp.swt \ +swt/rap/org.argeo.tp.swt.workbench \ +org.argeo.tp.jcr \ +org.argeo.cms \ +swt/org.argeo.cms \ +swt/rap/org.argeo.cms \ +$(A2_CATEGORY) + +clean: + rm -rf $(BUILD_BASE) + +include $(SDK_SRC_BASE)/sdk/argeo-build/osgi.mk \ No newline at end of file diff --git a/branch.mk b/branch.mk new file mode 100644 index 0000000..a6f8488 --- /dev/null +++ b/branch.mk @@ -0,0 +1 @@ +include $(SDK_SRC_BASE)/cnf/unstable.bnd diff --git a/cnf/unstable.bnd b/cnf/unstable.bnd new file mode 100644 index 0000000..359edf6 --- /dev/null +++ b/cnf/unstable.bnd @@ -0,0 +1,6 @@ +MAJOR=2 +MINOR=3 +MICRO=9 +qualifier=.next + +Bundle-RequiredExecutionEnvironment=JavaSE-11 diff --git a/configure b/configure new file mode 100755 index 0000000..47f7d96 --- /dev/null +++ b/configure @@ -0,0 +1,7 @@ +#!/bin/sh + +# Source are located where this script is +SDK_SRC_BASE="$(cd "$(dirname "$0")"; pwd -P)" + +# Source the configure script +. $SDK_SRC_BASE/sdk/argeo-build/configure diff --git a/org.argeo.cms.jcr.ui/.classpath b/org.argeo.cms.jcr.ui/.classpath new file mode 100644 index 0000000..81fe078 --- /dev/null +++ b/org.argeo.cms.jcr.ui/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/org.argeo.cms.jcr.ui/.project b/org.argeo.cms.jcr.ui/.project new file mode 100644 index 0000000..645296b --- /dev/null +++ b/org.argeo.cms.jcr.ui/.project @@ -0,0 +1,28 @@ + + + org.argeo.cms.jcr.ui + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms.jcr.ui/META-INF/.gitignore b/org.argeo.cms.jcr.ui/META-INF/.gitignore new file mode 100644 index 0000000..4854a41 --- /dev/null +++ b/org.argeo.cms.jcr.ui/META-INF/.gitignore @@ -0,0 +1 @@ +/MANIFEST.MF diff --git a/org.argeo.cms.jcr.ui/bnd.bnd b/org.argeo.cms.jcr.ui/bnd.bnd new file mode 100644 index 0000000..c3c609c --- /dev/null +++ b/org.argeo.cms.jcr.ui/bnd.bnd @@ -0,0 +1,23 @@ +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.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,\ +javax.servlet.*;version="[3,5)",\ +* + +## 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.jcr.ui/build.properties b/org.argeo.cms.jcr.ui/build.properties new file mode 100644 index 0000000..c6baffa --- /dev/null +++ b/org.argeo.cms.jcr.ui/build.properties @@ -0,0 +1,5 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + icons/ diff --git a/org.argeo.cms.jcr.ui/icons/loading.gif b/org.argeo.cms.jcr.ui/icons/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..3288d1035d70bb86517e2c233f1a904e41f06b29 GIT binary patch literal 3208 zcmc(iX;4#H9>pJdFE7h`I{IF)0|5<6L}(j=N}5%L009EB2nYfyF)E0PvIqo$u!IC; z4PgyY5|S9AEh38G)(9eq4TbH7_UHg@yWrlIJ$6smIADL7s^P;_O;ykRc9soXl`UC*LwQJXkii*0rx|*7rI2=x7WaRkx_~XZqFJ8R3c=2Kg zf@aSAv8+BJ8+^hyay>(QR@t*blbKzsf0}bscEqRc5Hd3o(-N5RyW=zWB*zQw6Zh>* z2CROCDAbu#D`)S|J_o(lL9Yn3l*+8RdiRD_>iNz$#_IAzCna&Wl5 zSF_(rRCDD!wi#i8oAm&jYtn2_@VB%2-H*G%bN#|(6R6N?wM)3u`PiGzwuX7qmTgyF zpE)h0kuoxQ9?=kW7Y!=R@DmhU9)vwT*EZWzJ zrt+=2tqFts72yIp?|gvdLhs8Hfku^Z(){gmN%Y=K#P|%fkvgUj~HfIp3CuXqCtYGtJ#me+n+-LmP( z*XNuk%!aH8bIE@_Bj46>M*dSro|7<6vZ7WUHh5YQzN$>IJFqCb|CT!wj~R2C2%=q{ zpt8rzY$aw?W?=Ustv{jo?Ow@ZRkLe<)NItY>Cyhle*wR59dTdF6(@{5^ zAQBOB*hNtc3bkY-8{Cm$nFS@elbTtSqrt7MB{h_4y+~`!mVa}?c&N>&?P}GqdMuhQ z&@TD5Czd((DcG_Su~dKKV)Pj$-qi1WHM8_vc^O4?^!oY|tmK~i!{fjd&@_1E(T~r7 z_REZy&hMT^ySJB3W7l$4YhR`M(J7S5S~+4Q&3HPa)z%zPpisOp$^ zTEe99ig2$5_qFr!$;7A6CJ}PJmRhli>w?LC}Y`#HLGy6 zMU4EhL~dKCN5Ut;U2jd*83ShBNiu zcJB0l9>1Modc?-oM<R4?}3g}UJ%@K);kriq>)e*rh%hdqM)5Q)*+O8 zXm;SEbs@koiYS!9YXIclSg+5m_s~yrW#kKMdiRszg(gCP5HPmP7L)vCf8@fxUh6qY z@Z#TmkjzAZX{rwE+q|K~F2v5{_@vt%>yT_a#fF03SFt{0RXvDAiaY~K9CgS1O>frXgAjBCS}mEd4mIWZ$=ovd5| zR?GRdU}d6+Q`+JRW)|=v7$)XNkn3yE`!nAiSCvOB1jKT zG<1aK3s<0b0m==egTD#8i(Of=1pGDTOCho0XpIOMQ&P87cVKY1W=C6kIg z9cH=@a&zbm2+`|{(_?YC9fdm?1TY~-pwlBn?>=(~1pDKbco6jloP;0-cqRiwV1A_S zEyV0Dj8Pwy!nekzaN>{)7rgZ&_QLxK{~1yRe865^yx>}+a!ECd>#MMwddow z@CU{l+Rt$xuXuf}?ga{3IAr?Raql^c@a%sI0U5m}HvJ5O1#I%_MMPt#BH>OqUZ{-k zt>4Xzz=%jT*FVW(uYkWyx}9Gw$HdN*qU?Bit#ji(Wi7p-u|_8?h^%szIS^s^fNM}b zgGy>|=cbEufpguY5_6w~&ZLv=Bo06UF9EYIY;Er-1VK)SyF&!|J{axiE1z^(hXwVq zsFS=K-#zC}CcOs^8W{KAt+kK)jYDgDYbCXv{{rwsgqtIU3<910$CJi)s?? z_t8k{>7*0~4l~LLF7$WXT5OSq5QCTbP_l!SN|{R}3D&eWA8~0ltWh1IL+ZBX4rRSt zWF6Om3WDMu4xK^1(BF`2cL}rUCzhHAB`@j5&R-yk_l*t;mPGY|u2^o|myvcOdrg0W z%=lX;f^Vkqfp?u7*4qQq%A3Mpf!xspWBSKS@O%r*TSM}?dl(@*%{0Jm_8;(h{R__M BtPP;ntUtgtM-08hLcx>H*4ev1) zGPuHf&FXJIUHm0pWUuVD>RYZ)@a6RfD-?XO#ar?YV(>(x;&$>}-3Z}NVUmqIk(i`& z&VF{!i0;w27L{8ume)M;mU`8(vyu@mGl4Y1d)y(G%ell?!7m z_&LWieS!=2++L2i7_^zRR~lr||$JU+EJtGkqI z8xh+SVLh@sg&J+2g;B+;bqpmQr45))irFcZjq_irv~;_+CSelrD|v$cxQDK(S?^NE zw`>=mXT?5WZgu;zUe80>?)Ui~w7{(Px}lLCt@!lA)eoDCtcMK$oOW~abY3t)SEo)7 zGzgEiN7L1BM%m=572fEcJ-%f4mSS{+c2Vr(^oWtEkHq?BMUOa6f=H00qv8W1;^TjQ zFKPN?58>0Ml*w7G;lrE$J}~2VO1eWhp%xAyAU0$Dkq09ssi`t%G(+2XD(5v-1MY`jRZt z@hNqWIJ4(jA5W86wt+2UHRSnIgt|u*xv%;*wjgkB+e7zs`QsJSWZJW3V!Nm4P@;aTv3jGhmS&y;8I@!$Qs#DbD)R!A*@9#^>IC zv@hG8K?*)-)<^f!yA}?JU6d6Zly4@8+k)nHswI}#bX3agc=^rVY7yPCLXOi^Unr?& zmp$?es`mSgiG7Q;DOjn~#qb-c_OrqG*fLCIHU?(%_dFEr@9neKw5{encsas!5!fGU!7TW{1i`VB& zDRZW3iSKPZfvHU?ekZE^{{Eq@bRn-i19=IIisII6=apanp$gCPrm{o}B4h9e#L}!T$ z<-4E30&zlzlB!#f*quHj7xsLq!~EX;a;Hm;XeW-($eAGR&}r8`uDQ@9D-z43;j>I+ zK#j>qiE-aXm5(6q2Vljhd=M&peeuf-bg>ohptaGtVT|r@#UgbB;?gV(a_VqV&i1VJ zE7nsSB(SH@N(xW$8_-kql4HLhjrPc7pJhbArM!K+Zt6*MT1lm3P=P&-ZbdR80;~P{ zFcww}kkig@!i;C4bUOrKl}=&CQ_&V}n3|9!&X>mN**yA985B>_U-0N-G6Qjh8EUfY z8-CsyL7q2~n+4LF9hMSdT8D39OjNV+!(|e&lC&gCUk@nD-+;Ln#rsYCm_V$yg$M z-XLmS`$N+$2Hc_l(9}fe$5UC>sJu)3f;vIo0VGodGDV&@u96&5ggKjhW4MK9IEh$K zf(khoAr1Ik%s*5l_X265hMf`#)L5S4tCqd%i ze~x@ot5*X@26n>A?fgjZ1vm#{FDb^ObUC|lJ1p}Y*}6?B#>};ayNlP3+@`=9kZ>wa zTGOf|c=qO|$cx&eC8;Ovek`T=mOm~I1NL1w!%xc6Q5g|-iA<2SACseujshaX|x zb0I(*Ij~ehZRd4u;>B1ZmRUupjB@hPfT=xm+j`=xwt_#a^|*dnB3-_Z`zm0;wUF!V3sYm z^UxqBx@7>V7lXv{fNAS~aG#&9LqoJR3}WiP@c*FSg+k+Iq01(tAfsNd&`@lR2h~O+ zz`z^L0Vnb&(a@Q|>;6U2?;r9sO0N9hWcY?$jDa#dH-igI`TE8KWG<%y*lyH?MmZ=G zO{(r=Qy5$Oi*aaD9pZdZqjM~!Np(H*bBQK3o0tN#q92VWH4P7~@q9N9lA3yFE)-XU4A6i15VVWSRa&c%$czpy(b$lR|ya^_?L8BhjU-54pAg4<2P{*!k zgM^+mlInxm_0qSkD3Mh`qEBsskV38DiftW`2N{e!H7+lm&>=KP$g?uw$DJ&Bf%#890 zi}3btR2byxc-4!d7u*{W8Mbeml%}?UvBS1tAPq}K zMxK%jCwgSU?Z`+_87aT_2mCpE& z3|mRT0cyQ|o!oaS@FLRVq~euB@5A zRA`cjT`Lv3`Wo5yvQ((EDdW};OIw;EO{GFrMu>mDlnTA3$xT<23Z1cuBuRz3U6XD5 zVX6FQ_Lu?()6oUp@*;6Z7y}{hi9rWZH*RL~4><{se)A_hXtROdtnMQF9+aC9o*}T! zvc8;3K_hS}Aoisat;OKz1CA!CXyL%IgOyJ)L^)?EZm~~MAOEyOFJ?4M z0x~gnvVWFG&x&nmw+>Dr8GecRi$lHlO!0f`8eT0tAO9v|rn>(yqi#6jO40ui3WO}XHVw-5Cf|9O}$5Udah7pxoQ2j=J=aCU!_8fXnMyx_80c=>XPaT5*vsdO4it-rw?$C zz96hv{|P<%hh}{`V68_DR$&Y844&9jdCcbtc^$B&Q)0lQ?>yGAzn8GqlJOYH@vil| zDA@HzHiNMu8Q2laH-93!Wsvim4A^5cCIflpPj{DT>*I=LOlZq1CGX(d3iuH2(e1p5 zY!c|9Y{6iwm0Poj7x7lQ^;@^(L#3olO?qnEoAN(-5mT@YflEcAKkuQZ z!*&5e@`=y8EvRV$oktbiC(ZijJo+@8OTgA3pT`i?xDvqF2)5Mg1>N*%*onYa1D}^G zFprRKy&I?d_t3k}@T1@`wGVmfI=i@CwCCq|hV_)|7ww_q^VE`UPp4w^8w2Ln8#4^m zrK{~dKp$+&;GCGRo(>g)UJS0e6>X>jI(KCocve#TMFlmo@n#Nq`$v8%)%`jF-Iem- zo$8ZxGGTBalMz?A1?VkB+1R%k5tpj_>)>P#_+%bv(97npyS?tB7mYtMbqGgl<42tbSYiN^rG zbxle~aRBJOfafu!)1~-1#PL$RA9UOU`=poxaj+D7ibLnBUSLgS6zt{qfj;#UQy}g_ z>7i9~cf#ftnu75`!9Mi%+y$HX{c~03(&b8Y^jOr>hMIdliNE2?3n#*z%b)=EBb%Rd zUxWD>w3A|teigw_%cfxhR1mKCr3LNSjNBvCe6Nqf&o6!%2gL*q1i@)zDhuZK`!L@A z;XN>kto#Oh_WeidiN?C~)=_8Mhz4*N2T%QJ{>QWvIZe{Z3+veg<^e*q_3AEGCp#eZVMH3FCnUVu!SKv9Q2qaisj z0N6MbjrNmMU<7>j3PzQE5>9$Qa3G4`qI49mgCA}RY*#o3tb;ST0y`DOaJp4s@2>@N zez!xhKEyA{C@$uD@H{@iONyfugWmrWpN06+7)nQRFTAZd3Igf9L=>+?dEnj0QPzT) zz@tG*AA;CX${zq`0zH2?pJSOx!0n$H&IEdVDILXAKW->)pb(1Prv@56_&mb17eY@P z$xQ{6GZs(5&5tO$+-KYx12{QPt49bEmohejlK|t); z!4g*uXqd_-!q#!BqZq~RC>}+zH(~1ss$&a^Cy4HYRLB3IScKwy6uS`4%u^j>QS5=@ z{V3i=IP;b2=nC<<24ZH55`Hywypzb?u9obN+OQ_TVvQ410*!S$fOiR$t5%1Wj%Dyu z@f6F+Qt#o3(g$&tR2^4z<AN%Z116w>Z)$T{Hs#_Z&_HnrFTd^fA^dWMNRJ=PpT^1mahxrwJL|~JJxg34fRhu zgTJ|5SJec*V9KG7WW8g|uI6`uFEi&=btIn`JtqgBPJp|w<1b=wZqrr04j%%YiVVIb zo^ve*+|4vs-+)*HC#G0?z7|AUg-^WFswQ>%r0Uv-aIIpvN{rOnn2ER3+c)_U0c*o;!Pz7ul`N5!Qs;Iir6 z==0#0%JvP4yWcAwh*_NLbF~%c^p)n{l4zjbH-yu}&p#|mb?(DHT35+AoI4tsp-!!M zZj(87==sa^X>7M*z|Ucr}tbu2U`)^aY>TnY;VFl}+)$iY=Eb zQ`L=2+vQy}q8$7AYjvJ94ye5L3CLKQJ8Bfza#nG#)#dDBb>rf8ws1-IaFbAFZ*Kj? fb=>mrW32q8R6@7Vxk_3UrS5a{ZAF{-)U%S;26-~;A5FAa4R8-cLj-`&SNESg5VIPQm zR7ahX2rG)PiYttQR(BgL#KZ)(6@luYP7w%T(Gdp-P$0k~_TJ~r*4@>u?)q1|e`LtJ z=iGbFJ@3bF2L2#l9cm|-E+7cPP8t>*K@emHet2A{d{M_Khaa4s5ut%ZRpX2&@W5Au zt=~xyw$AwB5JktQLeVBsx+=t`-FAxoH2;{?*cK@95<}J{2F1t4#qLNXMs5{o34&TD z4gOVRN=t9s`|&ewymP(tsB6|{XTz-D`4e`$-!HGL;GJxubegCBeRl_%D{NzCrl-;w zU+xMyQgw&Viy_}XsgpzwP}#~r)vEe}Yd)iWyB?>1+Sa!#%aGOfpl0m;Z+?;Q@{bt) zOpZKXzoa@Oglg7W(IM+k%7u#n4^y2ZfJTPvF#=;XrQvbK4-No^-S4A`;dtBsrs=bk>f zb}C`NL|QyFT7IIS{`}aLiD|d4rTWj|shStlhONua_bwz>uCIJHz|G7be-xi&nY=4x zsPXfUc`B9Oc-X?uQKAak(q8R8h4dY6G&XI| z&6Y^hhej_=aJ=OaO^hZsD3TlAQ>az<@eWF)n!)6Qa|Rqm6*5cJLT6A*{bzqs&>vxl z*iyC3)gB5DQG>G#%q~g)HgryqxZlFC(B+K zuumI<;Jm4b*#1<03z_JJY_g_YHd^NNkei(+nXTP!(L>&*LXCIOcqo~v7ZQ$nl1_&V z9u#dbeJeOp44ghE3y~+`)A;hla0rnoLZ0e}(>fh+oXHQynVn%A!_VNkSm3w`XcD}M z2?6@aMmvG%#!GItDy2jWM-DVX1T?}0Y2MW9`!*ZUID^h#l!68IC9q(p?5#NZwsDF^ z=XqjqURMInyJX;e02Ze*aq~P0t{&|9!tOubP@NDn>Vk|}DFG^Fqfj8~cu_R@GDojT zd#Q00%Eh;aA-^@L|L!PqKJMlkj(cT0ValIl!jYo?`SPbZuK{2}geQ zr}7(RQn!hv54f=zGHi$x&8GM@2_eADv&CFNh8}b8bjT!2O_;qo zsFVPr9C@N2NP{ahlk~LUJczz<~2&S;2_e)gfAKu@saMP=wq zC>~t3JT6@RToUYfzJc*PF*=9a?b}k;aJiwSeXn#x{@tnLKdP^34rm|=OMoP7(ItZ| z2K-W~D9~rFZ(8 z3Xq+~uXV*CTgZ40&OWAdXJJ264#Et-))C-ltIS0NXpDO*Hp8$AHxmAaf=4ff4?g;l z&rxqv3p7Q<{b}OF7&>8=~&_1*|&~5x)DQD^;`zjBebdsUB^pk=1`PmbCa>f1y$JxLZ9Ob-ExT zs!|ov^B3l9YaGyGg~Ime0%d}BcF*qJYui6Qa5g!^X1I97mXQjoZPU)YQyZ|y)RS=b z^p{(PhsH|=J8y22_Ee43rY)bLcG!@f^k~r0JQ%aFaes|is&PtLcj)F3wa!LdU0p8R w;4RgxtshOM#pH$BUBAvx-Eo1NA2A`GetrKE%Sypq_`d@o4Otyr6&SPk-&%i1JOBUy literal 0 HcmV?d00001 diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/CmsUiConstants.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/CmsUiConstants.java new file mode 100644 index 0000000..fd1dda5 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/CmsUiConstants.java @@ -0,0 +1,23 @@ +package org.argeo.cms.ui; + +/** Commons constants */ +@Deprecated +public interface CmsUiConstants { + // DATAKEYS +// public final static String STYLE = EclipseUiConstants.CSS_CLASS; +// public final static String MARKUP = EclipseUiConstants.MARKUP_SUPPORT; + @Deprecated + /* RWT.CUSTOM_ITEM_HEIGHT */ + public final static String ITEM_HEIGHT = "org.eclipse.rap.rwt.customItemHeight"; + + // EVENT DETAILS + @Deprecated + /* RWT.HYPERLINK */ + public final static int HYPERLINK = 1 << 26; + + // STANDARD RESOURCES + public final static String LOADING_IMAGE = "icons/loading.gif"; + + // MISCEALLENEOUS + String DATE_TIME_FORMAT = "dd/MM/yyyy, HH:mm"; +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/CmsUiProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/CmsUiProvider.java new file mode 100644 index 0000000..5f2377b --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/CmsUiProvider.java @@ -0,0 +1,51 @@ +package org.argeo.cms.ui; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.api.acr.Content; +import org.argeo.cms.jcr.acr.JcrContent; +import org.argeo.cms.swt.acr.SwtUiProvider; +import org.argeo.jcr.JcrException; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** Stateless factory building an SWT user interface given a JCR context. */ +public interface CmsUiProvider extends SwtUiProvider { + /** + * Initialises a user interface. + * + * @param parent the parent composite + * @param context a context node (holding the JCR underlying session), or null + */ + default Control createUi(Composite parent, Node context) throws RepositoryException { + // does nothing by default + return null; + } + + default Control createUiPart(Composite parent, Node context) { + try { + return createUi(parent, context); + } catch (RepositoryException e) { + throw new JcrException("Cannot create UI for context " + context, e); + } + } + + @Override + default Control createUiPart(Composite parent, Content context) { + if (context == null) + return createUiPart(parent, (Node) null); + if (context instanceof JcrContent) { + Node node = ((JcrContent) context).getJcrNode(); + return createUiPart(parent, node); + } else { +// CmsLog.getLog(CmsUiProvider.class) +// .warn("In " + getClass() + ", content " + context + " is not compatible with JCR."); +// return createUiPart(parent, (Node) null); + + throw new IllegalArgumentException( + "In " + getClass() + ", content " + context + " is not compatible with JCR"); + } + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/LifeCycleUiProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/LifeCycleUiProvider.java new file mode 100644 index 0000000..5d77c15 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/LifeCycleUiProvider.java @@ -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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java new file mode 100644 index 0000000..4ce4688 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/AbstractFormPart.java @@ -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 + * true 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 false + */ + 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 true if the part is dirty, false + * 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 true if the part is stale, false + * 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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormColors.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormColors.java new file mode 100644 index 0000000..32b031b --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormColors.java @@ -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 IFormColors.TITLE. + */ + public static final String TITLE = IFormColors.TITLE; + + /** + * Key for the tree/table border color. + * + * @deprecated use IFormColors.BORDER + */ + public static final String BORDER = IFormColors.BORDER; + + /** + * Key for the section separator color. + * + * @deprecated use IFormColors.SEPARATOR. + */ + public static final String SEPARATOR = IFormColors.SEPARATOR; + + /** + * Key for the section title bar background. + * + * @deprecated use IFormColors.TB_BG + */ + public static final String TB_BG = IFormColors.TB_BG; + + /** + * Key for the section title bar foreground. + * + * @deprecated use IFormColors.TB_FG + */ + public static final String TB_FG = IFormColors.TB_FG; + + /** + * Key for the section title bar gradient. + * + * @deprecated use IFormColors.TB_GBG + */ + public static final String TB_GBG = IFormColors.TB_GBG; + + /** + * Key for the section title bar border. + * + * @deprecated use IFormColors.TB_BORDER. + */ + 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 IFormColors.TB_TOGGLE. + */ + public static final String TB_TOGGLE = IFormColors.TB_TOGGLE; + + /** + * Key for the section toggle hover color. + * + * @deprecated use IFormColors.TB_TOGGLE_HOVER. + */ + 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 initializeColorTable(). + * + * @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 SWT class. + * + * @param code + * the system color constant as defined in SWT + * 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. + * + *

+ * 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 true if background is white, false + * 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 null if + * not in the registry. + * + * @param key + * the color key + * @return color object if found, or null 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 true if shared, false 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 true if at least one of the primary colors in the + * source RGB are within the provided range, false + * 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 true if at least two of the primary colors in the + * source RGB are within the provided range, false + * 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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java new file mode 100644 index 0000000..9e931ba --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormFonts.java @@ -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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java new file mode 100644 index 0000000..9927104 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormToolkit.java @@ -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. + *

+ * 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. + *

+ * The toolkit creates some of the most common controls used to populate Eclipse + * forms. Controls that must be created using their constructors, + * adapt() method is available to change its properties in the + * same way as with the supported toolkit controls. + *

+ * 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). + *

+ * FormToolkit is normally instantiated, but can also be subclassed if some of + * the methods needs to be modified. In those cases, super 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). + *

+ * 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 not marked as + * shared via the markShared() method. + *

+ * 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 null) + * @param style + * the button style (for example, SWT.PUSH) + * @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 true, 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 true, form will be scrolled horizontally + * and/or vertically if needed to ensure that the control is + * visible when it gains focus. Set it to false if + * the control is not capable of gaining focus. + * @param trackKeyboard + * if true, 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 + * false 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. +// *

+// * 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: +// * +// *

+//	 *
+//	 *
+//	 *
+//	 *             widget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER);
+//	 *
+//	 *             or
+//	 *
+//	 *             widget.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TEXT_BORDER);
+//	 *
+//	 *
+//	 *
+//	 * 
+// * +// * @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 + * SWT.BORDER or SWT.NULL + * @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 true, 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 SWT.NULL, + * SWT.LEFT_TO_RIGHT and SWT.RIGHT_TO_LEFT. + * + * @return orientation style for this toolkit, or SWT.NULL 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 SWT.NULL, SWT.LEFT_TO_RIGHT + * and SWT.RIGHT_TO_LEFT. + * + * @param orientation + * style for this toolkit. + */ + + public void setOrientation(int orientation) { + this.orientation = orientation; + } +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java new file mode 100644 index 0000000..76e3f11 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/FormUtil.java @@ -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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java new file mode 100644 index 0000000..cf0e5d3 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IFormColors.java @@ -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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java new file mode 100644 index 0000000..954cc03 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IFormPart.java @@ -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). + *

+ * The form part has two 'out of sync' states in respect to the model(s) that + * feed the form: dirty and stale. 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). + *

+ * 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 true if the part has selected and revealed the + * input object, false 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). + *

+ * 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 true if the part needs refreshing, + * false 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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java new file mode 100644 index 0000000..490d3a3 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IManagedForm.java @@ -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. + *

+ * 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. + *

+ * 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 true, 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 true if the form contains this object, + * false otherwise. + */ + boolean setInput(Object input); + + /** + * Returns the current page input. + * + * @return page input object or null if not applicable. + */ + Object getInput(); + + /** + * Tests if form is dirty. A managed form is dirty if at least one managed + * part is dirty. + * + * @return true if at least one managed part is dirty, + * false 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 true if the form is stale, false + * 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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java new file mode 100644 index 0000000..0f557d4 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/IPartSelectionListener.java @@ -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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java new file mode 100644 index 0000000..4140465 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/ManagedForm.java @@ -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. + *

+ * 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. + *

+ * 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 Display.syncExec or + * asyncExec. + */ + 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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java new file mode 100644 index 0000000..484dae8 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormEditor.java @@ -0,0 +1,85 @@ +package org.argeo.cms.ui.eclipse.forms.editor; + +import org.argeo.cms.ui.eclipse.forms.FormToolkit; +import org.eclipse.jface.dialogs.IPageChangeProvider; + +/** + * 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. + *

+ * 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. + *

+ * Subclasses should extend this class and implement addPages + * method. One of the two addPage 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 + * IFormPage 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.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java new file mode 100644 index 0000000..a788412 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/FormPage.java @@ -0,0 +1,276 @@ +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.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 + * initialize 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 true if the page is currently active, + * false 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 + * createFormContent(IManagedForm) 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 null- form page has no title image. Subclasses + * may override. + * + * @return null + */ + 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 true if the managed form is dirty, + * false 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 false + */ + 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 true if the page has been successfully selected + * and revealed by one of the managed form parts, false + * 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 true + */ + public boolean canLeaveThePage() { + return true; + } +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java new file mode 100644 index 0000000..eb08cb5 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/eclipse/forms/editor/IFormPage.java @@ -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: + *

    + *
  • The form page has a managed form
  • + *
  • The form page has a unique id
  • + *
  • The form page can be GUI but can also wrap a complete + * editor class (in that case, it should return true + * from isEditor() method).
  • + *
  • The form page is lazy i.e. understands that + * its part control will be created at the last possible + * moment.
  • . + *
+ *

Existing editors can be wrapped by implementing + * this interface. In this case, 'isEditor' should return true. + * A common editor to wrap in TextEditor 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 null 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 + * false) or lazily create and/or populate the content on + * true. + * + * @param active + * true if page should be visible, false + * otherwise. + */ + void setActive(boolean active); + /** + * Returns true if page is currently active, false if not. + * + * @return true 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 true if the editor can flip to another page, + * false otherwise. + */ + boolean canLeaveThePage(); + /** + * Returns the control associated with this page. + * + * @return the control of this page if created or null 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 true if the page wraps an editor, + * false 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, false should be returned to allow another + * page to try. + * + * @param object + * object to select and reveal + * @return true if the request was successful, false + * otherwise. + */ + boolean selectReveal(Object object); +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditableLink.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditableLink.java new file mode 100644 index 0000000..3c1e8cd --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditableLink.java @@ -0,0 +1,75 @@ +package org.argeo.cms.ui.forms; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.swt.SwtEditablePart; +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 + SwtEditablePart { + 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.jcr.ui/src/org/argeo/cms/ui/forms/EditableMultiStringProperty.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditableMultiStringProperty.java new file mode 100644 index 0000000..ff82700 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditableMultiStringProperty.java @@ -0,0 +1,261 @@ +package org.argeo.cms.ui.forms; + +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.cms.ui.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 SwtEditablePart { + 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 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 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 getValues() { + return values; + } + + public void setValues(List 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(CmsSwtUtils.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); + CmsSwtUtils.markup(label); + CmsSwtUtils.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); + CmsSwtUtils.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); + CmsSwtUtils.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); + CmsSwtUtils.style(lbl, style); + CmsSwtUtils.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() { + CmsSwtUtils.style(getControl(), FormStyle.propertyText.style()); +// getControl().setData(STYLE, FormStyle.propertyText.style()); + super.startEditing(); + } + + public synchronized void stopEditing() { + CmsSwtUtils.style(getControl(), FormStyle.propertyMessage.style()); +// 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.jcr.ui/src/org/argeo/cms/ui/forms/EditablePropertyDate.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditablePropertyDate.java new file mode 100644 index 0000000..641f916 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditablePropertyDate.java @@ -0,0 +1,298 @@ +package org.argeo.cms.ui.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.swt.CmsSwtUtils; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.cms.ui.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 SwtEditablePart { + 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()) + CmsSwtUtils.style(getControl(), FormStyle.propertyText); +// getControl().setData(STYLE, FormStyle.propertyText.style()); + super.startEditing(); + } + + public synchronized void stopEditing() { + if (EclipseUiUtils.isEmpty(dateTxt.getText())) + CmsSwtUtils.style(getControl(), FormStyle.propertyMessage); +// getControl().setData(STYLE, FormStyle.propertyMessage.style()); + else + CmsSwtUtils.style(getControl(), FormStyle.propertyText); +// 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(CmsSwtUtils.fillWidth()); + CmsSwtUtils.style(lbl, style); + CmsSwtUtils.markup(lbl); + if (mouseListener != null) + lbl.addMouseListener(mouseListener); + return lbl; + } + + private Control createCustomEditableControl(Composite box, String style) { + box.setLayoutData(CmsSwtUtils.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); + CmsSwtUtils.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); + CmsSwtUtils.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 + CmsSwtUtils.markup(CalendarPopup.this); + CmsSwtUtils.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.jcr.ui/src/org/argeo/cms/ui/forms/EditablePropertyString.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditablePropertyString.java new file mode 100644 index 0000000..f2575e1 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/EditablePropertyString.java @@ -0,0 +1,80 @@ +package org.argeo.cms.ui.forms; + +import static org.argeo.cms.ui.forms.FormStyle.propertyMessage; +import static org.argeo.cms.ui.forms.FormStyle.propertyText; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.cms.ui.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 SwtEditablePart { + private static final long serialVersionUID = 5055000749992803591L; + + private String propertyName; + private String message; + + // encode the '&' character in rap + private final static String AMPERSAND = "&"; + 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); + //setUseTextAsLabel(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("
", "\n")); + } + } + + public synchronized void startEditing() { + CmsSwtUtils.style(getControl(), FormStyle.propertyText); + super.startEditing(); + } + + public synchronized void stopEditing() { + if (EclipseUiUtils.isEmpty(((Text) getControl()).getText())) + CmsSwtUtils.style(getControl(), FormStyle.propertyMessage); + else + CmsSwtUtils.style(getControl(), FormStyle.propertyText); + super.stopEditing(); + } + + public String getPropertyName() { + return propertyName; + } +} \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormConstants.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormConstants.java new file mode 100644 index 0000000..fe9f7e7 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormConstants.java @@ -0,0 +1,7 @@ +package org.argeo.cms.ui.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.jcr.ui/src/org/argeo/cms/ui/forms/FormEditorHeader.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormEditorHeader.java new file mode 100644 index 0000000..a75c191 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormEditorHeader.java @@ -0,0 +1,114 @@ +package org.argeo.cms.ui.forms; + +import java.util.Observable; +import java.util.Observer; + +import javax.jcr.Node; + +import org.argeo.api.cms.ux.CmsEditable; +import org.argeo.cms.swt.CmsSwtUtils; +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); + + CmsSwtUtils.style(display, FormStyle.header.style()); + display.setBackgroundMode(SWT.INHERIT_FORCE); + + display.setLayout(CmsSwtUtils.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); + CmsSwtUtils.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.jcr.ui/src/org/argeo/cms/ui/forms/FormPageViewer.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormPageViewer.java new file mode 100644 index 0000000..1888055 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormPageViewer.java @@ -0,0 +1,608 @@ +package org.argeo.cms.ui.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.argeo.api.cms.CmsLog; +import org.argeo.api.cms.ux.Cms2DSize; +import org.argeo.api.cms.ux.CmsEditable; +import org.argeo.api.cms.ux.CmsImageManager; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.cms.ui.viewers.AbstractPageViewer; +import org.argeo.cms.ui.viewers.Section; +import org.argeo.cms.ui.viewers.SectionPart; +import org.argeo.cms.ui.widgets.EditableImage; +import org.argeo.cms.ui.widgets.Img; +import org.argeo.cms.ui.widgets.StyledControl; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.jcr.JcrException; +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 CmsLog log = CmsLog.getLog(FormPageViewer.class); + private static final long serialVersionUID = 5277789504209413500L; + + private final Section mainSection; + + // TODO manage within the CSS + private Integer labelColWidth = null; + 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(SwtEditablePart part, Object caretPosition) { + if (part instanceof Img) { + ((Img) part).setFileUploadListener(fileUploadListener); + } + } + + /** To be overridden.Save the edited part. */ + protected void save(SwtEditablePart part) throws RepositoryException { + Node node = null; + if (part instanceof EditableMultiStringProperty) { + EditableMultiStringProperty ept = (EditableMultiStringProperty) part; + // SWT : View + List 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, true); + node.getSession().save(); + } + } + + @Override + protected void updateContent(SwtEditablePart part) throws RepositoryException { + if (part instanceof EditableMultiStringProperty) { + EditableMultiStringProperty ept = (EditableMultiStringProperty) part; + // SWT : View + Node node = ept.getNode(); + String propName = ept.getPropertyName(); + List valStrings = new ArrayList(); + 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 IllegalStateException("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; + SwtEditablePart 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(); + SwtEditablePart 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(SwtEditablePart 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 = (CmsImageManager) CmsSwtUtils.getCmsView(mainSection).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(CmsSwtUtils.fillWidth()); + section.setLayout(CmsSwtUtils.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(CmsSwtUtils.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 valueStrings = new ArrayList(); + + 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(CmsSwtUtils.fillWidth()); + } + } + + protected Label createPropertyLbl(Composite parent, String value) { + return createPropertyLbl(parent, value, SWT.NONE); + } + + protected Label createPropertyLbl(Composite parent, String value, int vAlign) { + // boolean isSmall = CmsView.getCmsView(parent).getUxContext().isSmall(); + Label label = new Label(parent, SWT.LEAD | SWT.WRAP); + label.setText(value + " "); + CmsSwtUtils.style(label, FormStyle.propertyLabel.style()); + GridData gd = new GridData(SWT.LEAD, vAlign, false, false); + if (labelColWidth != null) + 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); + CmsSwtUtils.style(label, style); + return label; + } + + protected Composite createRowLayoutComposite(Composite parent) throws RepositoryException { + Composite bodyRow = new Composite(parent, SWT.NO_FOCUS); + bodyRow.setLayoutData(CmsSwtUtils.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 { + + 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; + + imageManager().uploadImage(context, context, cleanedName, stream, details.getContentType()); + // 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 JcrException("Unable to refresh " + "image section for " + context, re); + } + } + }); + } + } + + 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, new Cms2DSize(preferredSize.x, preferredSize.y)) { + private static final long serialVersionUID = 1297900641952417540L; + + @Override + protected void setContainerLayoutData(Composite composite) { + composite.setLayoutData(CmsSwtUtils.grabWidth(SWT.CENTER, SWT.DEFAULT)); + } + + @Override + protected void setControlLayoutData(Control control) { + control.setLayoutData(CmsSwtUtils.grabWidth(SWT.CENTER, SWT.DEFAULT)); + } + }; + img.setLayoutData(CmsSwtUtils.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(CmsSwtUtils.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); + CmsSwtUtils.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 JcrException("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); + SwtEditablePart ep = findDataParent(btn); + if (ep != null && ep instanceof EditableMultiStringProperty) { + EditableMultiStringProperty emsp = (EditableMultiStringProperty) ep; + List 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 JcrException("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", "
"); + // 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.jcr.ui/src/org/argeo/cms/ui/forms/FormStyle.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormStyle.java new file mode 100644 index 0000000..709ecd0 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormStyle.java @@ -0,0 +1,29 @@ +package org.argeo.cms.ui.forms; + +import org.argeo.api.cms.ux.CmsStyle; + +/** Syles used */ +public enum FormStyle implements CmsStyle { + // 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; + + @Override + public String getClassPrefix() { + return "argeo-form"; + } + + // TODO clean button style management + public final static String BUTTON_SUFFIX = "_btn"; +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormUtils.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormUtils.java new file mode 100644 index 0000000..eeafabb --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/FormUtils.java @@ -0,0 +1,196 @@ +package org.argeo.cms.ui.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.argeo.api.cms.CmsLog; +import org.argeo.api.cms.ux.CmsView; +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.ui.util.CmsUiUtils; +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 CmsLog log = CmsLog.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 = CmsUiUtils.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 the link + */ + public static String getPhoneLink(String value, String label) { + StringBuilder builder = new StringBuilder(); + builder.append("").append(label) + .append(""); + 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 the link + */ + public static String getMailLink(String value, String label) { + StringBuilder builder = new StringBuilder(); + value = replaceAmpersand(value); + builder.append("").append(label).append(""); + 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("" + label + ""); + return builder.toString(); + } + + private static String AMPERSAND = "&"; + + /** + * Cleans a String by replacing any '&' by its HTML encoding '&#38;' to + * avoid SAXParseException 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.jcr.ui/src/org/argeo/cms/ui/forms/MarkupValidatorCopy.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/MarkupValidatorCopy.java new file mode 100644 index 0000000..3f588d1 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/MarkupValidatorCopy.java @@ -0,0 +1,169 @@ +package org.argeo.cms.ui.forms; + +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.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. + */ +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 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(""); + markup.append(text); + markup.append(""); + 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(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append(""); + result.append("]>"); + return result.toString(); + } + + private static Map createSupportedElementsMap() { + Map result = new HashMap(); + 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 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.jcr.ui/src/org/argeo/cms/ui/forms/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/package-info.java new file mode 100644 index 0000000..5f954c1 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/forms/package-info.java @@ -0,0 +1,2 @@ +/** Argeo CMS forms, based on SWT/JFace. */ +package org.argeo.cms.ui.forms; \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/CmsFsBrowser.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/CmsFsBrowser.java new file mode 100644 index 0000000..d9c1c12 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/CmsFsBrowser.java @@ -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.auth.CurrentUser; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.swt.CmsSwtUtils; +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.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 = CmsJcrUtils.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 pea = new PrivilegedExceptionAction() { + @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 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 + " >> "); + CmsSwtUtils.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) { + CmsSwtUtils.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 = "Unknown"; + 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 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.jcr.ui/src/org/argeo/cms/ui/fs/FileDrop.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/FileDrop.java new file mode 100644 index 0000000..e875b5a --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/FileDrop.java @@ -0,0 +1,37 @@ +package org.argeo.cms.ui.fs; + +import java.io.IOException; +import java.io.InputStream; + +import org.argeo.api.cms.CmsLog; +import org.argeo.eclipse.ui.specific.FileDropAdapter; +import org.eclipse.swt.dnd.DND; +import org.eclipse.swt.dnd.DropTarget; +import org.eclipse.swt.dnd.DropTargetEvent; +import org.eclipse.swt.widgets.Control; + +/** Allows a control to receive file drops. */ +public class FileDrop { + private final static CmsLog log = CmsLog.getLog(FileDrop.class); + + public void createDropTarget(Control control) { + FileDropAdapter fileDropAdapter = new FileDropAdapter() { + @Override + protected void processUpload(InputStream in, String fileName, String contentType) throws IOException { + if (log.isDebugEnabled()) + log.debug("Process upload of " + fileName + " (" + contentType + ")"); + processFileUpload(in, fileName, contentType); + } + }; + DropTarget dropTarget = new DropTarget(control, DND.DROP_MOVE | DND.DROP_COPY); + fileDropAdapter.prepareDropTarget(control, dropTarget); + } + + public void handleFileDrop(Control control, DropTargetEvent event) { + } + + /** Executed in UI thread */ + protected void processFileUpload(InputStream in, String fileName, String contentType) throws IOException { + + } +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/FsContextMenu.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/FsContextMenu.java new file mode 100644 index 0000000..1fb3c2a --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/FsContextMenu.java @@ -0,0 +1,383 @@ +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.argeo.api.cms.CmsLog; +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.swt.CmsSwtUtils; +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 CmsLog log = CmsLog.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 actionButtons = new HashMap(); + + 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()); + CmsSwtUtils.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()); + CmsSwtUtils.markup(btn); + CmsSwtUtils.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 iterator = selection.iterator(); + List 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.jcr.ui/src/org/argeo/cms/ui/fs/FsStyles.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/FsStyles.java new file mode 100644 index 0000000..9ae3192 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/FsStyles.java @@ -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.jcr.ui/src/org/argeo/cms/ui/fs/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/package-info.java new file mode 100644 index 0000000..6a6c272 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/fs/package-info.java @@ -0,0 +1,2 @@ +/** SWT/JFace file system components. */ +package org.argeo.cms.ui.fs; \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/Activator.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/Activator.java new file mode 100644 index 0000000..e10da3a --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/Activator.java @@ -0,0 +1,37 @@ +package org.argeo.cms.ui.internal; + +import org.argeo.api.cms.CmsState; +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; + + // @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, CmsState.class, null); + nodeState.open(); + } + + @Override + public void stop(BundleContext context) throws Exception { + if (nodeState != null) { + nodeState.close(); + nodeState = null; + } + } + + public static CmsState getNodeState() { + return nodeState.getService(); + } +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/JcrContentProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/JcrContentProvider.java new file mode 100644 index 0000000..ea0abdf --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/JcrContentProvider.java @@ -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.swt.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 arr = new ArrayList(); + 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 arr = new ArrayList(); + 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.jcr.ui/src/org/argeo/cms/ui/internal/JcrFileUploadReceiver.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/JcrFileUploadReceiver.java new file mode 100644 index 0000000..60bb42b --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/JcrFileUploadReceiver.java @@ -0,0 +1,74 @@ +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.api.cms.ux.CmsImageManager; +import org.argeo.cms.ui.widgets.Img; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.eclipse.rap.fileupload.FileDetails; +import org.eclipse.rap.fileupload.FileUploadReceiver; + +public class JcrFileUploadReceiver extends FileUploadReceiver { + private Img img; + private final Node parentNode; + private final String nodeName; + private final CmsImageManager imageManager; + + /** If nodeName is null, use the uploaded file name */ + public JcrFileUploadReceiver(Img img, Node parentNode, String nodeName, CmsImageManager imageManager) { + super(); + this.img = img; + 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(img.getNode(),parentNode, fileName, stream, contentType); + return; + } + + 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 JcrException("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.jcr.ui/src/org/argeo/cms/ui/internal/SimpleEditableImage.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/SimpleEditableImage.java new file mode 100644 index 0000000..6162a74 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/internal/SimpleEditableImage.java @@ -0,0 +1,74 @@ +package org.argeo.cms.ui.internal; + +import javax.jcr.RepositoryException; + +import org.argeo.api.cms.ux.Cms2DSize; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.ui.util.CmsUiUtils; +import org.argeo.cms.ui.widgets.EditableImage; +import org.argeo.cms.ux.AbstractImageManager; +import org.argeo.cms.ux.CmsUxUtils; +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 Cms2DSize imageSize; + + public SimpleEditableImage(Composite parent, int swtStyle) { + super(parent, swtStyle); + // load(getControl()); + getParent().layout(); + } + + public SimpleEditableImage(Composite parent, int swtStyle, String src, Cms2DSize 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 = CmsUxUtils.img(src, imageSize); + else + imgTag = CmsUiUtils.noImg(imageSize != null ? imageSize : AbstractImageManager.NO_IMAGE_SIZE); + return imgTag; + } + + protected Text createText(Composite box, String style) { + Text text = new Text(box, getStyle()); + CmsSwtUtils.style(text, style); + return text; + } + + public String getSrc() { + return src; + } + + public void setSrc(String src) { + this.src = src; + } + + public Cms2DSize getImageSize() { + return imageSize; + } + + public void setImageSize(Cms2DSize imageSize) { + this.imageSize = imageSize; + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java new file mode 100644 index 0000000..3806341 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/DefaultRepositoryRegister.java @@ -0,0 +1,75 @@ +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.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; + +public class DefaultRepositoryRegister extends Observable implements RepositoryRegister { + /** Key for a JCR repository alias */ + private final static String CN = CmsConstants.CN; + /** Key for a JCR repository URI */ + // public final static String JCR_REPOSITORY_URI = "argeo.jcr.repository.uri"; + private final static CmsLog log = CmsLog.getLog(DefaultRepositoryRegister.class); + + /** Read only map which will be directly exposed. */ + private Map repositories = Collections.unmodifiableMap(new TreeMap()); + + @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 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 map = new TreeMap(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 map = new TreeMap(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.jcr.ui/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java new file mode 100644 index 0000000..0f7ee77 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/FullVersioningTreeContentProvider.java @@ -0,0 +1,98 @@ +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 result = new ArrayList(); + 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 tmp = new ArrayList(); + 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.jcr.ui/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java new file mode 100644 index 0000000..b36acc3 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrBrowserUtils.java @@ -0,0 +1,68 @@ +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.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; + +/** 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.jcr.ui/src/org/argeo/cms/ui/jcr/JcrDClickListener.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrDClickListener.java new file mode 100644 index 0000000..1707681 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrDClickListener.java @@ -0,0 +1,60 @@ +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.jcr.ui/src/org/argeo/cms/ui/jcr/JcrImages.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrImages.java new file mode 100644 index 0000000..d1d1e31 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrImages.java @@ -0,0 +1,24 @@ +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("person-logged-in.png"); + 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.jcr.ui/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java new file mode 100644 index 0000000..cc8479f --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/JcrTreeContentProvider.java @@ -0,0 +1,82 @@ +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.util.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) { + Node rootNode = (Node) inputElement; + return childrenNodes(rootNode); + } + + 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 children = new ArrayList(); + NodeIterator nit = parentNode.getNodes(); + while (nit.hasNext()) { + Node node = nit.nextNode(); +// if (node.getName().startsWith("rep:") || node.getName().startsWith("jcr:") +// || node.getName().startsWith("nt:")) +// continue nodes; + children.add(node); + } + 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.jcr.ui/src/org/argeo/cms/ui/jcr/NodeContentProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/NodeContentProvider.java new file mode 100644 index 0000000..0625cc8 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/NodeContentProvider.java @@ -0,0 +1,175 @@ +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.api.cms.CmsConstants; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.security.Keyring; +import org.argeo.cms.ui.jcr.model.RepositoriesElem; +import org.argeo.cms.ui.jcr.model.SingleJcrNodeElem; +import org.argeo.cms.ux.widgets.TreeParent; +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 = CmsJcrUtils.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(), CmsConstants.EGO_REPOSITORY); + } + } + 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 objs = new ArrayList(); + 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 { + + 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.jcr.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java new file mode 100644 index 0000000..a5751c0 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/NodeLabelProvider.java @@ -0,0 +1,113 @@ +package org.argeo.cms.ui.jcr; + +import javax.jcr.NamespaceException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.nodetype.NodeType; + +import org.argeo.api.cms.CmsLog; +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.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 CmsLog log = CmsLog.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 { + // TODO check workspace type? + if (node.getDepth() == 1 && node.hasProperty(Property.JCR_ID)) + return JcrImages.HOME; + + // 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.jcr.ui/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java new file mode 100644 index 0000000..444350a --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/OsgiRepositoryRegister.java @@ -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 repositoryTracker; + + public OsgiRepositoryRegister() { + repositoryTracker = new ServiceTracker(bc, Repository.class, null) { + + @Override + public Repository addingService(ServiceReference reference) { + + Repository repository = super.addingService(reference); + Map props = new HashMap<>(); + for (String key : reference.getPropertyKeys()) { + props.put(key, reference.getProperty(key)); + } + register(repository, props); + return repository; + } + + @Override + public void removedService(ServiceReference reference, Repository service) { + Map 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.jcr.ui/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java new file mode 100644 index 0000000..fd544bb --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/PropertiesContentProvider.java @@ -0,0 +1,42 @@ +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.util.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 props = new TreeSet(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.jcr.ui/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java new file mode 100644 index 0000000..37b90f7 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/PropertyLabelProvider.java @@ -0,0 +1,101 @@ +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.CmsUiConstants; +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(CmsUiConstants.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 = ""; + 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.jcr.ui/src/org/argeo/cms/ui/jcr/RepositoryRegister.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/RepositoryRegister.java new file mode 100644 index 0000000..802c756 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/RepositoryRegister.java @@ -0,0 +1,16 @@ +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 getRepositories(); +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java new file mode 100644 index 0000000..37dfe2b --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/VersionLabelProvider.java @@ -0,0 +1,33 @@ +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.jcr.ui/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java new file mode 100644 index 0000000..d33b33f --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/MaintainedRepositoryElem.java @@ -0,0 +1,21 @@ +package org.argeo.cms.ui.jcr.model; + +import javax.jcr.Repository; + +import org.argeo.cms.ux.widgets.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.jcr.ui/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java new file mode 100644 index 0000000..908d1b1 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/RemoteRepositoryElem.java @@ -0,0 +1,76 @@ +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.cms.jcr.CmsJcrUtils; +import org.argeo.cms.security.Keyring; +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; + +/** 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 = CmsJcrUtils.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.jcr.ui/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java new file mode 100644 index 0000000..8c40f8b --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/RepositoriesElem.java @@ -0,0 +1,112 @@ +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.jcr.CmsJcrUtils; +import org.argeo.cms.security.Keyring; +import org.argeo.cms.ui.jcr.RepositoryRegister; +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; +import org.argeo.eclipse.ui.dialogs.ErrorFeedback; + +/** + * 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 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 = CmsJcrUtils.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.jcr.ui/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java new file mode 100644 index 0000000..296c369 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/RepositoryElem.java @@ -0,0 +1,98 @@ +package org.argeo.cms.ui.jcr.model; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; +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(CmsConstants.SYS_WORKSPACE); + 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.jcr.ui/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java new file mode 100644 index 0000000..a2584a5 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/SingleJcrNodeElem.java @@ -0,0 +1,84 @@ +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.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; + +/** + * 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.jcr.ui/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java new file mode 100644 index 0000000..2d78666 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/WorkspaceElem.java @@ -0,0 +1,117 @@ +package org.argeo.cms.ui.jcr.model; + +import javax.jcr.AccessDeniedException; +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.cms.ux.widgets.TreeParent; +import org.argeo.eclipse.ui.EclipseUiException; +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()) + try { + return session.getRootNode().hasNodes(); + } catch (AccessDeniedException e) { + // current user may not have access to the root node + return false; + } + 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.jcr.ui/src/org/argeo/cms/ui/jcr/model/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/package-info.java new file mode 100644 index 0000000..8f54744 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/model/package-info.java @@ -0,0 +1,2 @@ +/** Model for SWT/JFace JCR components. */ +package org.argeo.cms.ui.jcr.model; \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/package-info.java new file mode 100644 index 0000000..26ae330 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/jcr/package-info.java @@ -0,0 +1,2 @@ +/** SWT/JFace JCR components. */ +package org.argeo.cms.ui.jcr; \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/package-info.java new file mode 100644 index 0000000..82fdee7 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/package-info.java @@ -0,0 +1,2 @@ +/** SWT/JFace components for Argeo CMS. */ +package org.argeo.cms.ui; \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsLink.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsLink.java new file mode 100644 index 0000000..e91f9ba --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsLink.java @@ -0,0 +1,282 @@ +package org.argeo.cms.ui.util; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.ux.CmsStyle; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.ui.CmsUiProvider; +import org.argeo.jcr.JcrException; +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 CmsLog log = CmsLog.getLog(CmsLink.class); + private BundleContext bundleContext; + + private String label; + private String style; + private String target; + private String image; + private boolean openNew = false; + 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, (String) null); + } + + public CmsLink(String label, String target, CmsStyle style) { + this(label, target, style != null ? style.style() : null); + } + + public CmsLink(String label, String target, String style) { + super(); + this.label = label; + this.target = target; + this.style = style; + 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.NONE); + comp.setLayout(CmsSwtUtils.noSpaceGridLayout()); + + Label link = new Label(comp, SWT.NONE); + CmsSwtUtils.markup(link); + 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); + CmsSwtUtils.style(comp, style != null ? style : getDefaultStyle()); + CmsSwtUtils.style(link, style != null ? style : getDefaultStyle()); + + // label + StringBuilder labelText = new StringBuilder(); + if (loggedInTarget != null && isLoggedIn()) { + labelText.append(""); + } else if (target != null) { + labelText.append(""); + } + if (image != null) { + registerImageIfNeeded(); + String imageLocation = RWT.getResourceManager().getLocation(image); + labelText.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(""); + + 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(); + try (InputStream inputStream = res.openStream()) { + resourceManager.register(image, inputStream); + if (log.isTraceEnabled()) + log.trace("Registered image " + image); + } catch (IOException e) { + throw new RuntimeException("Cannot load image " + image, e); + } + } + } + + private ImageData loadImage() { + URL url = getImageUrl(); + ImageData result = null; + try (InputStream inputStream = url.openStream()) { + result = new ImageData(inputStream); + if (log.isTraceEnabled()) + log.trace("Loaded image " + image); + } catch (IOException e) { + throw new RuntimeException("Cannot load image " + image, e); + } + 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 IllegalStateException("No image " + image + " available."); + + return url; + } + + public void setBundleContext(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + public void setLabel(String label) { + this.label = label; + } + + public void setStyle(String style) { + this.style = style; + } + + /** @deprecated Use {@link #setStyle(String)} instead. */ + @Deprecated + public void setCustom(String custom) { + this.style = 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 IllegalArgumentException( + "Unsupported vertical alignment " + 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; + } + + public void setOpenNew(boolean openNew) { + this.openNew = openNew; + } + + protected String getDefaultStyle() { + return SimpleStyle.link.name(); + } +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsPane.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsPane.java new file mode 100644 index 0000000..fc0c821 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsPane.java @@ -0,0 +1,49 @@ +package org.argeo.cms.ui.util; + +import org.argeo.cms.swt.CmsSwtUtils; +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(CmsSwtUtils.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.jcr.ui/src/org/argeo/cms/ui/util/CmsUiUtils.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsUiUtils.java new file mode 100644 index 0000000..3522f1b --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/CmsUiUtils.java @@ -0,0 +1,192 @@ +package org.argeo.cms.ui.util; + +import java.io.IOException; +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.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.ux.Cms2DSize; +import org.argeo.api.cms.ux.CmsView; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.ui.CmsUiConstants; +import org.argeo.cms.ux.AbstractImageManager; +import org.argeo.cms.ux.CmsUxUtils; +import org.argeo.jcr.JcrUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.service.ResourceManager; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.layout.RowData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Table; + +/** Static utilities for the CMS framework. */ +public class CmsUiUtils { + // private final static Log log = LogFactory.getLog(CmsUiUtils.class); + + /* + * CMS VIEW + */ + + /** + * The CMS view related to this display, or null if none is available from this + * call. + * + * @deprecated Use {@link CmsSwtUtils#getCmsView(Composite)} instead. + */ + @Deprecated + public static CmsView getCmsView() { +// return UiContext.getData(CmsView.class.getName()); + return CmsSwtUtils.getCmsView(Display.getCurrent().getActiveShell()); + } + + 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 IllegalArgumentException("Cannot extract server base URL from " + request.getRequestURL(), e); + } + } + + // + public static String getDataUrl(Node node, HttpServletRequest request) { + try { + StringBuilder buf = getServerBaseUrl(request); + buf.append(getDataPath(node)); + return new URL(buf.toString()).toString(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("Cannot build data URL for " + node, e); + } + } + + /** A path in the node repository */ + public static String getDataPath(Node node) { + return getDataPath(CmsConstants.EGO_REPOSITORY, node); + } + + public static String getDataPath(String cn, Node node) { + return CmsJcrUtils.getDataPath(cn, node); + } + + /** Clean reserved URL characters for use in HTTP links. */ + public static String getDataPathForUrl(Node node) { + return CmsSwtUtils.cleanPathForUrl(getDataPath(node)); + } + + /** @deprecated Use rowData16px() instead. GridData should not be reused. */ + @Deprecated + public static RowData ROW_DATA_16px = new RowData(16, 16); + + + + /* + * FORM LAYOUT + */ + + + + @Deprecated + public static void setItemHeight(Table table, int height) { + table.setData(CmsUiConstants.ITEM_HEIGHT, height); + } + + // + // 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(Node fileNode, String width, String height) { + return img(null, fileNode, width, height); + } + + public static String img(String serverBase, Node fileNode, String width, String height) { +// String src = (serverBase != null ? serverBase : "") + NodeUtils.getDataPath(fileNode); + String src; + src = (serverBase != null ? serverBase : "") + getDataPathForUrl(fileNode); + return CmsUxUtils.imgBuilder(src, width, height).append("/>").toString(); + } + + public static String noImg(Cms2DSize size) { + ResourceManager rm = RWT.getResourceManager(); + return CmsUxUtils.img(rm.getLocation(AbstractImageManager.NO_IMAGE), size); + } + + public static String noImg() { + return noImg(AbstractImageManager.NO_IMAGE_SIZE); + } + +// public static Image noImage(Cms2DSize size) { +// ResourceManager rm = RWT.getResourceManager(); +// InputStream in = null; +// try { +// in = rm.getRegisteredContent(AbstractImageManager.NO_IMAGE); +// ImageData id = new ImageData(in); +// ImageData scaled = id.scaledTo(size.getWidth(), size.getHeight()); +// Image image = new Image(Display.getCurrent(), scaled); +// return image; +// } finally { +// try { +// in.close(); +// } catch (IOException e) { +// // silent +// } +// } +// } + + /** Lorem ipsum text to be used during development. */ + 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."; + + /** Singleton. */ + private CmsUiUtils() { + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/DefaultImageManager.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/DefaultImageManager.java new file mode 100644 index 0000000..b431fc3 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/DefaultImageManager.java @@ -0,0 +1,133 @@ +package org.argeo.cms.ui.util; + +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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; + +import org.apache.commons.io.IOUtils; +import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.ux.Cms2DSize; +import org.argeo.cms.swt.AbstractSwtImageManager; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.service.ResourceManager; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.widgets.Display; + +/** Manages only public images so far. */ +public class DefaultImageManager extends AbstractSwtImageManager { + private final static CmsLog log = CmsLog.getLog(DefaultImageManager.class); + + /** @return null if not available */ + @Override + public String getImageUrl(Node node) { + return CmsUiUtils.getDataPathForUrl(node); + } + + protected String getResourceName(Node node) { + try { + String workspace = node.getSession().getWorkspace().getName(); + if (node.hasNode(JCR_CONTENT)) + return workspace + '_' + node.getNode(JCR_CONTENT).getIdentifier(); + else + return workspace + '_' + node.getIdentifier(); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + public Binary getImageBinary(Node node) { + try { + if (node.isNodeType(NT_FILE)) { + return node.getNode(JCR_CONTENT).getProperty(JCR_DATA).getBinary(); + } else { + return null; + } + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + public Image getSwtImage(Node node) { + InputStream inputStream = null; + Binary binary = getImageBinary(node); + if (binary == null) + return null; + try { + inputStream = binary.getStream(); + return new Image(Display.getCurrent(), inputStream); + } catch (RepositoryException e) { + throw new JcrException(e); + } finally { + IOUtils.closeQuietly(inputStream); + JcrUtils.closeQuietly(binary); + } + } + + @Override + public String uploadImage(Node context, Node parentNode, String fileName, InputStream in, String contentType) { + 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); + inputStream = new ByteArrayInputStream(arr); + ImageData id = new ImageData(inputStream); + processNewImageFile(context, fileNode, id); + + String mime = contentType != null ? contentType : Files.probeContentType(Paths.get(fileName)); + if (mime != null) { + fileNode.getNode(JCR_CONTENT).setProperty(Property.JCR_MIMETYPE, mime); + } + 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 CmsUiUtils.getDataPath(fileNode); + } catch (IOException e) { + throw new RuntimeException("Cannot upload image " + fileName + " in " + parentNode, e); + } catch (RepositoryException e) { + throw new JcrException(e); + } finally { + IOUtils.closeQuietly(inputStream); + } + } + + /** Does nothing by default. */ + protected void processNewImageFile(Node context, Node fileNode, ImageData id) + throws RepositoryException, IOException { + } + + @Override + protected String noImg(Cms2DSize size) { + return CmsUiUtils.noImg(size); + } +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/MenuLink.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/MenuLink.java new file mode 100644 index 0000000..284d2bd --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/MenuLink.java @@ -0,0 +1,22 @@ +package org.argeo.cms.ui.util; + +import org.argeo.cms.swt.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.jcr.ui/src/org/argeo/cms/ui/util/SimpleCmsHeader.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleCmsHeader.java new file mode 100644 index 0000000..ab6a29f --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleCmsHeader.java @@ -0,0 +1,97 @@ +package org.argeo.cms.ui.util; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.swt.CmsStyles; +import org.argeo.cms.swt.CmsSwtUtils; +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 lead = new ArrayList(); + private List center = new ArrayList(); + private List end = new ArrayList(); + + 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(CmsSwtUtils.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 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(CmsSwtUtils.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 lead) { + this.lead = lead; + } + + public void setCenter(List center) { + this.center = center; + } + + public void setEnd(List end) { + this.end = end; + } + + public void setSubPartsSameWidth(Boolean subPartsSameWidth) { + this.subPartsSameWidth = subPartsSameWidth; + } + + public List getLead() { + return lead; + } + + public List getCenter() { + return center; + } + + public List getEnd() { + return end; + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleDynamicPages.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleDynamicPages.java new file mode 100644 index 0000000..c61a2fc --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleDynamicPages.java @@ -0,0 +1,118 @@ +package org.argeo.cms.ui.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.swt.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("" + context.getName() + ""); + 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("Children:"); + // 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("Properties:"); + // 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 = ""; + 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.jcr.ui/src/org/argeo/cms/ui/util/SimpleStaticPage.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleStaticPage.java new file mode 100644 index 0000000..63e504b --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleStaticPage.java @@ -0,0 +1,32 @@ +package org.argeo.cms.ui.util; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.swt.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.jcr.ui/src/org/argeo/cms/ui/util/SimpleStyle.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleStyle.java new file mode 100644 index 0000000..b5fca26 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SimpleStyle.java @@ -0,0 +1,8 @@ +package org.argeo.cms.ui.util; + +import org.argeo.api.cms.ux.CmsStyle; + +/** Simple styles used by the CMS UI utilities. */ +public enum SimpleStyle implements CmsStyle { + link; +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/StyleSheetResourceLoader.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/StyleSheetResourceLoader.java new file mode 100644 index 0000000..1e17dc9 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/StyleSheetResourceLoader.java @@ -0,0 +1,71 @@ +package org.argeo.cms.ui.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.swt.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 stylesheets = new LinkedHashMap(); + + 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.jcr.ui/src/org/argeo/cms/ui/util/SystemNotifications.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SystemNotifications.java new file mode 100644 index 0000000..5a00781 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/SystemNotifications.java @@ -0,0 +1,129 @@ +package org.argeo.cms.ui.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.swt.CmsException; +import org.argeo.cms.swt.CmsStyles; +import org.argeo.cms.swt.CmsSwtUtils; +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("&body=").append(encoded); + } catch (UnsupportedEncodingException e) { + mailToUrl.append("&body=").append("Could not encode: ") + .append(e.getMessage()); + } + Label mailTo = new Label(pane, SWT.NONE); + CmsSwtUtils.markup(mailTo); + mailTo.setText("Send details"); + 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.jcr.ui/src/org/argeo/cms/ui/util/UserMenu.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/UserMenu.java new file mode 100644 index 0000000..09aeff6 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/UserMenu.java @@ -0,0 +1,56 @@ +package org.argeo.cms.ui.util; + +import javax.jcr.Node; + +import org.argeo.cms.swt.CmsException; +import org.argeo.cms.swt.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) { + // FIXME pass CMS context + super(CmsUiUtils.getCmsView(), null); + 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.jcr.ui/src/org/argeo/cms/ui/util/UserMenuLink.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/UserMenuLink.java new file mode 100644 index 0000000..317a7b5 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/UserMenuLink.java @@ -0,0 +1,84 @@ +package org.argeo.cms.ui.util; + +import javax.jcr.Node; + +import org.argeo.cms.CmsMsg; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.swt.CmsStyles; +import org.argeo.cms.swt.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.jcr.ui/src/org/argeo/cms/ui/util/VerticalMenu.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/VerticalMenu.java new file mode 100644 index 0000000..7f846c9 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/VerticalMenu.java @@ -0,0 +1,44 @@ +package org.argeo.cms.ui.util; + +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.cms.swt.CmsSwtUtils; +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 items = new ArrayList(); + + @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(CmsSwtUtils.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 getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/package-info.java new file mode 100644 index 0000000..566df88 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/util/package-info.java @@ -0,0 +1,2 @@ +/** Argeo CMS UI utilities. */ +package org.argeo.cms.ui.util; \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/AbstractPageViewer.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/AbstractPageViewer.java new file mode 100644 index 0000000..e23846e --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/AbstractPageViewer.java @@ -0,0 +1,351 @@ +package org.argeo.cms.ui.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.argeo.api.cms.CmsLog; +import org.argeo.api.cms.ux.CmsEditable; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.cms.swt.widgets.ScrolledPage; +import org.argeo.jcr.JcrException; +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 CmsLog log = CmsLog.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 SwtEditablePart 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 (RepositoryException e) { + throw new JcrException("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()); + } + } + + public void layoutPage() { + if (page != null) + page.layout(true, true); + } + + protected void showControl(Control control) { + if (page != null && (page instanceof ScrolledPage)) + ((ScrolledPage) page).showControl(control); + } + + @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(SwtEditablePart part) throws RepositoryException { + } + + /** Prepare the edited part */ + protected void prepare(SwtEditablePart 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) () -> { + try { + if (cmsEditable.canEdit() && !readOnly) + mouseListener = createMouseListener(); + else + mouseListener = null; + refresh(getControl()); + // layout(getControl()); + if (!getControl().isDisposed()) + layoutPage(); + } catch (RepositoryException e) { + throw new JcrException("Cannot refresh", e); + } + return null; + }); + } + + @Override + public void setSelection(ISelection selection, boolean reveal) { + this.selection = selection; + } + + protected void updateContent(SwtEditablePart part) throws RepositoryException { + } + + // LOW LEVEL EDITION + protected void edit(SwtEditablePart part, Object caretPosition) { + try { + if (edited == part) + return; + + if (edited != null && edited != part) { + SwtEditablePart previouslyEdited = edited; + try { + stopEditing(true); + } catch (Exception e) { + notifyEditionException(e); + edit(previouslyEdited, caretPosition); + return; + } + } + + part.startEditing(); + edited = part; + updateContent(part); + prepare(part, caretPosition); + edited.getControl().addFocusListener(new FocusListener() { + private static final long serialVersionUID = 6883521812717097017L; + + @Override + public void focusLost(FocusEvent event) { + stopEditing(true); + } + + @Override + public void focusGained(FocusEvent event) { + } + }); + + layout(part.getControl()); + showControl(part.getControl()); + } catch (RepositoryException e) { + throw new JcrException("Cannot edit " + part, e); + } + } + + protected void stopEditing(Boolean save) { + 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; + } + + try { + if (save) + save(edited); + + edited.stopEditing(); + SwtEditablePart editablePart = edited; + Control control = ((SwtEditablePart) edited).getControl(); + edited = null; + // TODO make edited state management more robust + updateContent(editablePart); + layout(control); + } catch (RepositoryException e) { + throw new JcrException("Cannot stop editing", e); + } finally { + edited = null; + } + } + + // METHODS AVAILABLE TO EXTENDING CLASSES + protected void saveEdit() { + if (edited != null) + stopEditing(true); + } + + protected void cancelEdit() { + if (edited != null) + stopEditing(false); + } + + /** Layout this controls from the related base page. */ + public void layout(Control... controls) { + page.layout(controls); + } + + /** + * Find the first {@link SwtEditablePart} in the parents hierarchy of this control + */ + protected SwtEditablePart findDataParent(Control parent) { + if (parent instanceof SwtEditablePart) { + return (SwtEditablePart) parent; + } + if (parent.getParent() != null) + return findDataParent(parent.getParent()); + else + throw new IllegalStateException("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 IllegalStateException("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(), eToLog); +// 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 IllegalStateException("No subject associated with this viewer"); + return res; + } + + // GETTERS / SETTERS + public boolean isReadOnly() { + return readOnly; + } + + protected SwtEditablePart 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.jcr.ui/src/org/argeo/cms/ui/viewers/ItemPart.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/ItemPart.java new file mode 100644 index 0000000..4ca45d1 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/ItemPart.java @@ -0,0 +1,9 @@ +package org.argeo.cms.ui.viewers; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +/** An editable part related to a JCR Item */ +public interface ItemPart { + public Item getItem() throws RepositoryException; +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/JcrVersionCmsEditable.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/JcrVersionCmsEditable.java new file mode 100644 index 0000000..298fbde --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/JcrVersionCmsEditable.java @@ -0,0 +1,94 @@ +package org.argeo.cms.ui.viewers; + +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.api.cms.ux.CmsEditionEvent; +import org.argeo.cms.ux.AbstractCmsEditable; +import org.argeo.jcr.JcrException; +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 AbstractCmsEditable { + 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 JcrException("Cannot check whether " + nodePath + " is editing", e); + } + } + + @Override + public void startEditing() { + try { + versionManager.checkout(nodePath); +// setChanged(); + } catch (RepositoryException e1) { + throw new JcrException("Cannot publish " + nodePath, e1); + } + notifyListeners(new CmsEditionEvent(nodePath, CmsEditionEvent.START_EDITING, this)); + } + + @Override + public void stopEditing() { + try { + versionManager.checkin(nodePath); +// setChanged(); + } catch (RepositoryException e1) { + throw new JcrException("Cannot publish " + nodePath, e1); + } + notifyListeners(new CmsEditionEvent(nodePath, CmsEditionEvent.STOP_EDITING, this)); + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/NodePart.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/NodePart.java new file mode 100644 index 0000000..b51d4fc --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/NodePart.java @@ -0,0 +1,8 @@ +package org.argeo.cms.ui.viewers; + +import javax.jcr.Node; + +/** An editable part related to a node */ +public interface NodePart extends ItemPart { + public Node getNode(); +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/PropertyPart.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/PropertyPart.java new file mode 100644 index 0000000..793079e --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/PropertyPart.java @@ -0,0 +1,8 @@ +package org.argeo.cms.ui.viewers; + +import javax.jcr.Property; + +/** An editable part related to a JCR Property */ +public interface PropertyPart extends ItemPart { + public Property getProperty(); +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/Section.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/Section.java new file mode 100644 index 0000000..b27fa38 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/Section.java @@ -0,0 +1,166 @@ +package org.argeo.cms.ui.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.swt.CmsSwtUtils; +import org.argeo.cms.swt.SwtEditablePart; +import org.argeo.cms.ui.widgets.JcrComposite; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +/** A structured UI related to a JCR context. */ +public class Section extends JcrComposite { + 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) { + this(parent, findSection(parent), style, node); + } + + public Section(Section section, int style, Node node) { + this(section, section, style, node); + } + + protected Section(Composite parent, Section parentSection, int style, Node node) { + super(parent, style, node); + try { + this.parentSection = parentSection; + if (parentSection != null) { + relativeDepth = getNode().getDepth() - parentSection.getNode().getDepth(); + } else { + relativeDepth = 0; + } + setLayout(CmsSwtUtils.noSpaceGridLayout()); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot create section from " + node, e); + } + } + + public Map getSubSections() throws RepositoryException { + LinkedHashMap result = new LinkedHashMap(); + for (Control child : getChildren()) { + if (child instanceof Composite) { + collectDirectSubSections((Composite) child, result); + } + } + return Collections.unmodifiableMap(result); + } + + private void collectDirectSubSections(Composite composite, LinkedHashMap subSections) + throws RepositoryException { + if (composite == sectionHeader || composite instanceof SwtEditablePart) + 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 Composite createHeader() { + return createHeader(this); + } + + public Composite createHeader(Composite parent) { + if (sectionHeader != null) + sectionHeader.dispose(); + + sectionHeader = new Composite(parent, SWT.NONE); + sectionHeader.setLayoutData(CmsSwtUtils.fillWidth()); + sectionHeader.setLayout(CmsSwtUtils.noSpaceGridLayout()); + // sectionHeader.moveAbove(null); + // layout(); + return sectionHeader; + } + + 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 sectionPart = (SectionPart) child; + if (sectionPart.getPartId().equals(partId)) + return sectionPart; + } + } + return null; + } + + public SectionPart nextSectionPart(SectionPart sectionPart) { + Control[] children = getChildren(); + for (int i = 0; i < children.length; i++) { + if (sectionPart == children[i]) { + for (int j = i + 1; j < children.length; j++) { + if (children[i + 1] instanceof SectionPart) { + return (SectionPart) children[i + 1]; + } + } + +// 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.jcr.ui/src/org/argeo/cms/ui/viewers/SectionPart.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/SectionPart.java new file mode 100644 index 0000000..4278c83 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/SectionPart.java @@ -0,0 +1,10 @@ +package org.argeo.cms.ui.viewers; + +import org.argeo.cms.swt.SwtEditablePart; + +/** An editable part dynamically related to a Section */ +public interface SectionPart extends SwtEditablePart, NodePart { + public String getPartId(); + + public Section getSection(); +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/package-info.java new file mode 100644 index 0000000..2f07931 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/viewers/package-info.java @@ -0,0 +1,2 @@ +/** Argeo CMS generic viewers, based on JFace. */ +package org.argeo.cms.ui.viewers; \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/EditableImage.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/EditableImage.java new file mode 100644 index 0000000..95d9e8e --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/EditableImage.java @@ -0,0 +1,112 @@ +package org.argeo.cms.ui.widgets; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.ux.Cms2DSize; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.ui.util.CmsUiUtils; +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 CmsLog log = CmsLog.getLog(EditableImage.class); + + private Cms2DSize preferredImageSize; + private Boolean loaded = false; + + public EditableImage(Composite parent, int swtStyle) { + super(parent, swtStyle); + } + + public EditableImage(Composite parent, int swtStyle, Cms2DSize preferredImageSize) { + super(parent, swtStyle); + this.preferredImageSize = preferredImageSize; + } + + public EditableImage(Composite parent, int style, Node node, boolean cacheImmediately, Cms2DSize 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 CmsUiUtils + .noImg(preferredImageSize != null ? preferredImageSize : new Cms2DSize(getSize().x, getSize().y)); + } + + protected Label createLabel(Composite box, String style) { + Label lbl = new Label(box, getStyle()); + // lbl.setLayoutData(CmsUiUtils.fillWidth()); + CmsSwtUtils.markup(lbl); + CmsSwtUtils.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 = CmsUiUtils.noImg(preferredImageSize); + loaded = false; + } + + if (imgTag == null) { + loaded = false; + imgTag = CmsUiUtils.noImg(preferredImageSize); + } else + loaded = true; + if (control != null) { + ((Label) control).setText(imgTag); + control.setSize(preferredImageSize != null + ? new Point(preferredImageSize.getWidth(), preferredImageSize.getHeight()) + : getSize()); + } else { + loaded = false; + } + getParent().layout(); + return loaded; + } + + public void setPreferredSize(Cms2DSize size) { + this.preferredImageSize = size; + if (!loaded) { + load((Label) getControl()); + } + } + + protected Text createText(Composite box, String style) { + Text text = new Text(box, getStyle()); + CmsSwtUtils.style(text, style); + return text; + } + + public Cms2DSize getPreferredImageSize() { + return preferredImageSize; + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/EditableText.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/EditableText.java new file mode 100644 index 0000000..e3499ac --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/EditableText.java @@ -0,0 +1,145 @@ +package org.argeo.cms.ui.widgets; + +import javax.jcr.Item; +import javax.jcr.RepositoryException; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +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; + + private boolean editable = true; + + private Color highlightColor; + private Composite highlight; + + private boolean useTextAsLabel = false; + + public EditableText(Composite parent, int style) { + super(parent, style); + editable = !(SWT.READ_ONLY == (style & SWT.READ_ONLY)); + highlightColor = parent.getDisplay().getSystemColor(SWT.COLOR_GRAY); + } + + 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); + editable = !(SWT.READ_ONLY == (style & SWT.READ_ONLY)); + highlightColor = parent.getDisplay().getSystemColor(SWT.COLOR_GRAY); + } + + @Override + protected Control createControl(Composite box, String style) { + if (isEditing() && getEditable()) { + return createText(box, style, true); + } else { + if (useTextAsLabel) { + return createTextLabel(box, style); + } else { + return createLabel(box, style); + } + } + } + + protected Label createLabel(Composite box, String style) { + Label lbl = new Label(box, getStyle() | SWT.WRAP); + lbl.setLayoutData(CmsSwtUtils.fillWidth()); + if (style != null) + CmsSwtUtils.style(lbl, style); + CmsSwtUtils.markup(lbl); + if (mouseListener != null) + lbl.addMouseListener(mouseListener); + return lbl; + } + + protected Text createTextLabel(Composite box, String style) { + Text lbl = new Text(box, getStyle() | SWT.MULTI); + lbl.setEditable(false); + lbl.setLayoutData(CmsSwtUtils.fillWidth()); + if (style != null) + CmsSwtUtils.style(lbl, style); + CmsSwtUtils.markup(lbl); + if (mouseListener != null) + lbl.addMouseListener(mouseListener); + return lbl; + } + + protected Text createText(Composite box, String style, boolean editable) { + highlight = new Composite(box, SWT.NONE); + highlight.setBackground(highlightColor); + GridData highlightGd = new GridData(SWT.FILL, SWT.FILL, false, false); + highlightGd.widthHint = 5; + highlightGd.heightHint = 3; + highlight.setLayoutData(highlightGd); + + final Text text = new Text(box, getStyle() | SWT.MULTI | SWT.WRAP); + text.setEditable(editable); + GridData textLayoutData = CmsSwtUtils.fillWidth(); + // textLayoutData.heightHint = preferredHeight; + text.setLayoutData(textLayoutData); + if (style != null) + CmsSwtUtils.style(text, style); + text.setFocus(); + return text; + } + + @Override + protected void clear(boolean deep) { + if (highlight != null) + highlight.dispose(); + super.clear(deep); + } + + 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(); + } + + public String getText() { + Control child = getControl(); + + if (child instanceof Label) + return ((Label) child).getText(); + else if (child instanceof Text) + return ((Text) child).getText(); + else + throw new IllegalStateException("Unsupported control " + child.getClass()); + } + + /** @deprecated Use {@link #isEditable()} instead. */ + @Deprecated + public boolean getEditable() { + return isEditable(); + } + + public boolean isEditable() { + return editable; + } + + public void setUseTextAsLabel(boolean useTextAsLabel) { + this.useTextAsLabel = useTextAsLabel; + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/Img.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/Img.java new file mode 100644 index 0000000..41063fa --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/Img.java @@ -0,0 +1,155 @@ +package org.argeo.cms.ui.widgets; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.api.cms.ux.Cms2DSize; +import org.argeo.api.cms.ux.CmsImageManager; +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.ui.internal.JcrFileUploadReceiver; +import org.argeo.cms.ui.viewers.NodePart; +import org.argeo.cms.ui.viewers.Section; +import org.argeo.cms.ui.viewers.SectionPart; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +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.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, Cms2DSize preferredImageSize) throws RepositoryException { + this(Section.findSection(parent), parent, swtStyle, imgNode, preferredImageSize, null); + setStyle(TextStyles.TEXT_IMAGE); + } + + public Img(Composite parent, int swtStyle, Node imgNode) throws RepositoryException { + this(Section.findSection(parent), parent, swtStyle, imgNode, null, null); + setStyle(TextStyles.TEXT_IMAGE); + } + + public Img(Composite parent, int swtStyle, Node imgNode, CmsImageManager imageManager) + throws RepositoryException { + this(Section.findSection(parent), parent, swtStyle, imgNode, null, imageManager); + setStyle(TextStyles.TEXT_IMAGE); + } + + Img(Section section, Composite parent, int swtStyle, Node imgNode, Cms2DSize preferredImageSize, + CmsImageManager imageManager) throws RepositoryException { + super(parent, swtStyle, imgNode, false, preferredImageSize); + this.section = section; + this.imageManager = imageManager != null ? imageManager + : (CmsImageManager) CmsSwtUtils.getCmsView(section).getImageManager(); + CmsSwtUtils.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 JcrException("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) { + Node imgNode = getNode(); + boolean loaded = imageManager.load(imgNode, lbl, getPreferredImageSize()); + // getParent().layout(); + return loaded; + } + + protected Node getUploadFolder() { + return Jcr.getParent(getNode()); + } + + protected String getUploadName() { + Node node = getNode(); + return Jcr.getName(node) + '[' + Jcr.getIndex(node) + ']'; + } + + protected CmsImageManager getImageManager() { + return imageManager; + } + + protected Control createImageChooser(Composite box, String style) throws RepositoryException { + JcrFileUploadReceiver receiver = new JcrFileUploadReceiver(this, getUploadFolder(), getUploadName(), + imageManager); + if (currentUploadHandler != null) + currentUploadHandler.dispose(); + currentUploadHandler = prepareUpload(receiver); + final ServerPushSession pushSession = new ServerPushSession(); + final FileUpload fileUpload = new FileUpload(box, SWT.NONE); + CmsSwtUtils.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.jcr.ui/src/org/argeo/cms/ui/widgets/JcrComposite.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/JcrComposite.java new file mode 100644 index 0000000..6b54c0a --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/JcrComposite.java @@ -0,0 +1,213 @@ +package org.argeo.cms.ui.widgets; + +import javax.jcr.Item; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.jcr.JcrException; +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 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) { + this(parent, style, item, false); + } + + public JcrComposite(Composite parent, int style, Item item, boolean cacheImmediately) { + super(parent, style); + if (item != null) + try { + 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 UnsupportedOperationException("Multiple properties not supported yet."); + this.property = property.getName(); + node = property.getParent(); + } + this.nodeId = node.getIdentifier(); + if (cacheImmediately) + this.cache = node; +// } + setLayout(CmsSwtUtils.noSpaceGridLayout()); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot create composite from " + item, e); + } + } + + public synchronized Node getNode() { + try { + if (!itemIsNode()) + throw new IllegalStateException("Item is not a Node"); + return getNodeInternal(); + } catch (RepositoryException e) { + throw new JcrException("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 String getPropertyName() { + try { + return getProperty().getName(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get property name", e); + } + } + + public synchronized Node getPropertyNode() { + try { + return getProperty().getNode(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get property name", e); + } + } + + public synchronized Property getProperty() { + try { + if (itemIsNode()) + throw new IllegalStateException("Item is not a Property"); + Node node = getNodeInternal(); + if (!node.hasProperty(property)) + throw new IllegalStateException("Property " + property + " is not set on " + node); + return node.getProperty(property); + } catch (RepositoryException e) { + throw new JcrException("Cannot get property " + property + " from node " + nodeId, e); + } + } + + public synchronized boolean itemIsNode() { + return property == null; + } + + public synchronized boolean itemExists() { + if (session == null) + return false; + try { + Node n = session.getNodeByIdentifier(nodeId); + if (!itemIsNode()) + return n.hasProperty(property); + else + return true; + } catch (ItemNotFoundException e) { + return false; + } catch (RepositoryException e) { + throw new JcrException("Cannot check whether node exists", e); + } + } + + /** Set/update the cache or change the node */ + public synchronized void setNode(Node node) { + if (!itemIsNode()) + throw new IllegalArgumentException("Cannot set a Node on a Property"); + + if (node == null) {// clear cache + this.cache = null; + return; + } + + try { +// if (session != null || session != node.getSession())// check session +// throw new IllegalArgumentException("Uncompatible session"); +// if (session == null) + session = node.getSession(); + if (nodeId == null || !nodeId.equals(node.getIdentifier())) { + nodeId = node.getIdentifier(); + cache = node; + itemUpdated(); + } else { + cache = node;// set/update cache + } + } catch (RepositoryException e) { + throw new IllegalStateException(e); + } + } + + /** Set/update the cache or change the property */ + public synchronized void setProperty(Property prop) { + if (itemIsNode()) + throw new IllegalArgumentException("Cannot set a Property on a Node"); + + if (prop == null) {// clear cache + this.cache = null; + return; + } + + try { + if (session == null || session != prop.getSession())// check session + throw new IllegalArgumentException("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 + } + } catch (RepositoryException e) { + throw new IllegalStateException(e); + } + } + + 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.jcr.ui/src/org/argeo/cms/ui/widgets/StyledControl.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/StyledControl.java new file mode 100644 index 0000000..e3a5cb4 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/StyledControl.java @@ -0,0 +1,153 @@ +package org.argeo.cms.ui.widgets; + +import javax.jcr.Item; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.ui.CmsUiConstants; +import org.argeo.eclipse.ui.specific.EclipseUiSpecificUtils; +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 CmsUiConstants { + 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; + + private Composite ancestorToLayout; + + public StyledControl(Composite parent, int swtStyle) { + super(parent, swtStyle); + setLayout(CmsSwtUtils.noSpaceGridLayout()); + } + + public StyledControl(Composite parent, int style, Item item) { + super(parent, style, item); + } + + public StyledControl(Composite parent, int style, Item item, boolean cacheImmediately) { + super(parent, style, item, cacheImmediately); + } + + protected abstract Control createControl(Composite box, String style); + + protected Composite createBox() { + Composite box = new Composite(container, SWT.INHERIT_DEFAULT); + setContainerLayoutData(box); + box.setLayout(CmsSwtUtils.noSpaceGridLayout(3)); + return box; + } + + protected Composite createContainer() { + Composite container = new Composite(this, SWT.INHERIT_DEFAULT); + setContainerLayoutData(container); + container.setLayout(CmsSwtUtils.noSpaceGridLayout()); + return container; + } + + 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) EclipseUiSpecificUtils.getStyleData(control); + clear(false); + refreshControl(style); + + // 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) EclipseUiSpecificUtils.getStyleData(control); + clear(false); + refreshControl(style); + } + + protected void refreshControl(String style) { + control = createControl(box, style); + setControlLayoutData(control); + if (ancestorToLayout != null) + ancestorToLayout.layout(true, true); + else + getParent().layout(true, true); + } + + public void setStyle(String style) { + Object currentStyle = null; + if (control != null) + currentStyle = EclipseUiSpecificUtils.getStyleData(control); + if (currentStyle != null && currentStyle.equals(style)) + return; + + clear(true); + refreshControl(style); + + if (style != null) { + CmsSwtUtils.style(box, style + "_box"); + CmsSwtUtils.style(container, style + "_container"); + } + } + + /** To be overridden */ + protected void setControlLayoutData(Control control) { + control.setLayoutData(CmsSwtUtils.fillWidth()); + } + + /** To be overridden */ + protected void setContainerLayoutData(Composite composite) { + composite.setLayoutData(CmsSwtUtils.fillWidth()); + } + + protected void clear(boolean deep) { + if (deep) { + for (Control control : getChildren()) + control.dispose(); + container = createContainer(); + box = createBox(); + } 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); + } + + public void setAncestorToLayout(Composite ancestorToLayout) { + this.ancestorToLayout = ancestorToLayout; + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/TextStyles.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/TextStyles.java new file mode 100644 index 0000000..e461ed0 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/TextStyles.java @@ -0,0 +1,37 @@ +package org.argeo.cms.ui.widgets; + +/** 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.jcr.ui/src/org/argeo/cms/ui/widgets/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/package-info.java new file mode 100644 index 0000000..514f753 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/cms/ui/widgets/package-info.java @@ -0,0 +1,2 @@ +/** Argeo CMS generic widgets, based on SWT. */ +package org.argeo.cms.ui.widgets; \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/AbstractNodeContentProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/AbstractNodeContentProvider.java new file mode 100644 index 0000000..fdafa98 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/AbstractNodeContentProvider.java @@ -0,0 +1,138 @@ +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.argeo.api.cms.CmsLog; +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 CmsLog log = CmsLog + .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 filterChildren(List children) + throws RepositoryException { + return children; + } + + protected Object[] getChildren(Node node) throws RepositoryException { + List nodes = new ArrayList(); + 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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java new file mode 100644 index 0000000..b880a63 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/AsyncUiEventListener.java @@ -0,0 +1,83 @@ +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.argeo.api.cms.CmsLog; +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 CmsLog logThis = CmsLog.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 events) throws RepositoryException; + + /** + * Whether these events should be processed in the UI or skipped with no UI + * job created. + */ + protected Boolean willProcessInUiThread(List events) throws RepositoryException { + return true; + } + + protected CmsLog getLog() { + return logThis; + } + + public final void onEvent(final EventIterator eventIterator) { + final List events = new ArrayList(); + 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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/DefaultNodeLabelProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/DefaultNodeLabelProvider.java new file mode 100644 index 0000000..22ffeaf --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/DefaultNodeLabelProvider.java @@ -0,0 +1,82 @@ +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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/JcrUiUtils.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/JcrUiUtils.java new file mode 100644 index 0000000..420154b --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/JcrUiUtils.java @@ -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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeColumnLabelProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeColumnLabelProvider.java new file mode 100644 index 0000000..7e12bec --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeColumnLabelProvider.java @@ -0,0 +1,123 @@ +package org.argeo.eclipse.ui.jcr; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; + +/** Simplifies writing JCR-based column label provider. */ +public class NodeColumnLabelProvider extends ColumnLabelProvider { + private static final long serialVersionUID = -6586692836928505358L; + + protected String getNodeText(Node node) throws RepositoryException { + return super.getText(node); + } + + protected String getNodeToolTipText(Node node) throws RepositoryException { + return super.getToolTipText(node); + } + + protected Image getNodeImage(Node node) throws RepositoryException { + return super.getImage(node); + } + + protected Font getNodeFont(Node node) throws RepositoryException { + return super.getFont(node); + } + + public Color getNodeBackground(Node node) throws RepositoryException { + return super.getBackground(node); + } + + public Color getNodeForeground(Node node) throws RepositoryException { + return super.getForeground(node); + } + + @Override + public String getText(Object element) { + try { + if (element instanceof Node) + return getNodeText((Node) element); + else if (element instanceof NodeElement) + return getNodeText(((NodeElement) element).getNode()); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + + @Override + public Image getImage(Object element) { + try { + if (element instanceof Node) + return getNodeImage((Node) element); + else if (element instanceof NodeElement) + return getNodeImage(((NodeElement) element).getNode()); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + + @Override + public String getToolTipText(Object element) { + try { + if (element instanceof Node) + return getNodeToolTipText((Node) element); + else if (element instanceof NodeElement) + return getNodeToolTipText(((NodeElement) element).getNode()); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + + @Override + public Font getFont(Object element) { + try { + if (element instanceof Node) + return getNodeFont((Node) element); + else if (element instanceof NodeElement) + return getNodeFont(((NodeElement) element).getNode()); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + + @Override + public Color getBackground(Object element) { + try { + if (element instanceof Node) + return getNodeBackground((Node) element); + else if (element instanceof NodeElement) + return getNodeBackground(((NodeElement) element).getNode()); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + + @Override + public Color getForeground(Object element) { + try { + if (element instanceof Node) + return getNodeForeground((Node) element); + else if (element instanceof NodeElement) + return getNodeForeground(((NodeElement) element).getNode()); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeElement.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeElement.java new file mode 100644 index 0000000..787c92e --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeElement.java @@ -0,0 +1,8 @@ +package org.argeo.eclipse.ui.jcr; + +import javax.jcr.Node; + +/** An element which is related to a JCR {@link Node}. */ +public interface NodeElement { + Node getNode(); +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeElementComparer.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeElementComparer.java new file mode 100644 index 0000000..2f3d64d --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodeElementComparer.java @@ -0,0 +1,36 @@ +package org.argeo.eclipse.ui.jcr; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.eclipse.ui.EclipseUiException; +import org.eclipse.jface.viewers.IElementComparer; + +/** Element comparer for JCR node, to be used in JFace viewers. */ +public class NodeElementComparer implements IElementComparer { + + public boolean equals(Object a, Object b) { + try { + if ((a instanceof Node) && (b instanceof Node)) { + Node nodeA = (Node) a; + Node nodeB = (Node) b; + return nodeA.getIdentifier().equals(nodeB.getIdentifier()); + } else { + return a.equals(b); + } + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot compare nodes", e); + } + } + + public int hashCode(Object element) { + try { + if (element instanceof Node) + return ((Node) element).getIdentifier().hashCode(); + return element.hashCode(); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot get hash code", e); + } + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodesWrapper.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodesWrapper.java new file mode 100644 index 0000000..2f808a5 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/NodesWrapper.java @@ -0,0 +1,71 @@ +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 getWrappedNodes() throws RepositoryException { + List nodes = new ArrayList(); + 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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/QueryTableContentProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/QueryTableContentProvider.java new file mode 100644 index 0000000..934fa67 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/QueryTableContentProvider.java @@ -0,0 +1,35 @@ +package org.argeo.eclipse.ui.jcr; + +import javax.jcr.NodeIterator; +import javax.jcr.RepositoryException; +import javax.jcr.query.Query; + +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** Content provider based on a JCR {@link Query}. */ +public class QueryTableContentProvider implements IStructuredContentProvider { + private static final long serialVersionUID = 760371460907204722L; + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + Query query = (Query) inputElement; + try { + NodeIterator nit = query.execute().getNodes(); + return JcrUtils.nodeIteratorToList(nit).toArray(); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/RowColumnLabelProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/RowColumnLabelProvider.java new file mode 100644 index 0000000..70c71ef --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/RowColumnLabelProvider.java @@ -0,0 +1,111 @@ +package org.argeo.eclipse.ui.jcr; + +import javax.jcr.RepositoryException; +import javax.jcr.query.Row; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; + +/** Simplifies writing JCR-based column label provider. */ +public class RowColumnLabelProvider extends ColumnLabelProvider { + private static final long serialVersionUID = -6586692836928505358L; + + protected String getRowText(Row row) throws RepositoryException { + return super.getText(row); + } + + protected String getRowToolTipText(Row row) throws RepositoryException { + return super.getToolTipText(row); + } + + protected Image getRowImage(Row row) throws RepositoryException { + return super.getImage(row); + } + + protected Font getRowFont(Row row) throws RepositoryException { + return super.getFont(row); + } + + public Color getRowBackground(Row row) throws RepositoryException { + return super.getBackground(row); + } + + public Color getRowForeground(Row row) throws RepositoryException { + return super.getForeground(row); + } + + @Override + public String getText(Object element) { + try { + if (element instanceof Row) + return getRowText((Row) element); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + + @Override + public Image getImage(Object element) { + try { + if (element instanceof Row) + return getRowImage((Row) element); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + + @Override + public String getToolTipText(Object element) { + try { + if (element instanceof Row) + return getRowToolTipText((Row) element); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + + @Override + public Font getFont(Object element) { + try { + if (element instanceof Row) + return getRowFont((Row) element); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + + @Override + public Color getBackground(Object element) { + try { + if (element instanceof Row) + return getRowBackground((Row) element); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + + @Override + public Color getForeground(Object element) { + try { + if (element instanceof Row) + return getRowForeground((Row) element); + else + throw new IllegalArgumentException("Unsupported element type " + element.getClass()); + } catch (RepositoryException e) { + throw new IllegalStateException("Repository exception when accessing " + element, e); + } + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/SimpleNodeContentProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/SimpleNodeContentProvider.java new file mode 100644 index 0000000..cb235d7 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/SimpleNodeContentProvider.java @@ -0,0 +1,59 @@ +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 basePaths; + private Boolean mkdirs = false; + + public SimpleNodeContentProvider(Session session, String... basePaths) { + this(session, Arrays.asList(basePaths)); + } + + public SimpleNodeContentProvider(Session session, List 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 baseNodes = new ArrayList(); + 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 getBasePaths() { + return basePaths; + } + + public void setMkdirs(Boolean mkdirs) { + this.mkdirs = mkdirs; + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/VersionColumnLabelProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/VersionColumnLabelProvider.java new file mode 100644 index 0000000..1ce3154 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/VersionColumnLabelProvider.java @@ -0,0 +1,80 @@ +package org.argeo.eclipse.ui.jcr; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.version.Version; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.swt.graphics.Image; + +/** Simplifies writing JCR-based column label provider. */ +public class VersionColumnLabelProvider extends ColumnLabelProvider { + private static final long serialVersionUID = -6117690082313161159L; + + protected String getVersionText(Version version) throws RepositoryException { + return super.getText(version); + } + + protected String getVersionToolTipText(Version version) throws RepositoryException { + return super.getToolTipText(version); + } + + protected Image getVersionImage(Version version) throws RepositoryException { + return super.getImage(version); + } + + protected String getUserName(Version version) throws RepositoryException { + Node node = version.getFrozenNode(); + if(node.hasProperty(Property.JCR_LAST_MODIFIED_BY)) + return node.getProperty(Property.JCR_LAST_MODIFIED_BY).getString(); + if(node.hasProperty(Property.JCR_CREATED_BY)) + return node.getProperty(Property.JCR_CREATED_BY).getString(); + return null; + } + +// protected String getActivityTitle(Version version) throws RepositoryException { +// Node activity = getActivity(version); +// if (activity == null) +// return null; +// if (activity.hasProperty("jcr:activityTitle")) +// return activity.getProperty("jcr:activityTitle").getString(); +// else +// return activity.getName(); +// } +// +// protected Node getActivity(Version version) throws RepositoryException { +// if (version.hasProperty(Property.JCR_ACTIVITY)) { +// return version.getProperty(Property.JCR_ACTIVITY).getNode(); +// } else +// return null; +// } + + @Override + public String getText(Object element) { + try { + return getVersionText((Version) element); + } catch (RepositoryException e) { + throw new RuntimeException("Runtime repository exception when accessing " + element, e); + } + } + + @Override + public Image getImage(Object element) { + try { + return getVersionImage((Version) element); + } catch (RepositoryException e) { + throw new RuntimeException("Runtime repository exception when accessing " + element, e); + } + } + + @Override + public String getToolTipText(Object element) { + try { + return getVersionToolTipText((Version) element); + } catch (RepositoryException e) { + throw new RuntimeException("Runtime repository exception when accessing " + element, e); + } + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/VersionHistoryContentProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/VersionHistoryContentProvider.java new file mode 100644 index 0000000..32e5d30 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/VersionHistoryContentProvider.java @@ -0,0 +1,27 @@ +package org.argeo.eclipse.ui.jcr; + +import javax.jcr.version.VersionHistory; + +import org.argeo.jcr.Jcr; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; + +/** Content provider based on a {@link VersionHistory}. */ +public class VersionHistoryContentProvider implements IStructuredContentProvider { + private static final long serialVersionUID = -4921107883428887012L; + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + VersionHistory versionHistory = (VersionHistory) inputElement; + return Jcr.getLinearVersions(versionHistory).toArray(); + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/WrappedNode.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/WrappedNode.java new file mode 100644 index 0000000..43df1fe --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/WrappedNode.java @@ -0,0 +1,41 @@ +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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/JcrColumnDefinition.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/JcrColumnDefinition.java new file mode 100644 index 0000000..c5dd733 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/JcrColumnDefinition.java @@ -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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/NodeViewerComparator.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/NodeViewerComparator.java new file mode 100644 index 0000000..d990460 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/NodeViewerComparator.java @@ -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: + * // IMPORTANT: initialize comparator before setting it + * JcrColumnDefinition firstCol = colDefs.get(0); + * comparator.setColumn(firstCol.getPropertyType(), + * firstCol.getPropertyName()); + * viewer.setComparator(comparator); + */ +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 the comparison + */ + @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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/RowViewerComparator.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/RowViewerComparator.java new file mode 100644 index 0000000..60d637d --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/RowViewerComparator.java @@ -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 the comparison + */ + @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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrNodeLabelProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrNodeLabelProvider.java new file mode 100644 index 0000000..aa2e337 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrNodeLabelProvider.java @@ -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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrRowLabelProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrRowLabelProvider.java new file mode 100644 index 0000000..5d421f6 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/SimpleJcrRowLabelProvider.java @@ -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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/package-info.java new file mode 100644 index 0000000..3678aab --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/lists/package-info.java @@ -0,0 +1,2 @@ +/** Generic SWT/JFace JCR utilities for lists. */ +package org.argeo.eclipse.ui.jcr.lists; \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/package-info.java new file mode 100644 index 0000000..19e3cc3 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/package-info.java @@ -0,0 +1,2 @@ +/** Generic SWT/JFace JCR utilities. */ +package org.argeo.eclipse.ui.jcr; \ No newline at end of file diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/JcrFileProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/JcrFileProvider.java new file mode 100644 index 0000000..c82e666 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/JcrFileProvider.java @@ -0,0 +1,129 @@ +package org.argeo.eclipse.ui.jcr.util; + +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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/JcrItemsComparator.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/JcrItemsComparator.java new file mode 100644 index 0000000..fb12399 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/JcrItemsComparator.java @@ -0,0 +1,21 @@ +package org.argeo.eclipse.ui.jcr.util; + +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 { + public int compare(Item o1, Item o2) { + try { + // TODO: put folder before files + return o1.getName().toLowerCase().compareTo(o2.getName().toLowerCase()); + } catch (RepositoryException e) { + throw new EclipseUiException("Cannot compare " + o1 + " and " + o2, e); + } + } + +} diff --git a/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/NodeViewerComparer.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/NodeViewerComparer.java new file mode 100644 index 0000000..54b795f --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/NodeViewerComparer.java @@ -0,0 +1,36 @@ +package org.argeo.eclipse.ui.jcr.util; + +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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/SingleSessionFileProvider.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/SingleSessionFileProvider.java new file mode 100644 index 0000000..291d579 --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/SingleSessionFileProvider.java @@ -0,0 +1,98 @@ +package org.argeo.eclipse.ui.jcr.util; + +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 + * JcrFileProvider , 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.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/package-info.java b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/package-info.java new file mode 100644 index 0000000..016348c --- /dev/null +++ b/org.argeo.cms.jcr.ui/src/org/argeo/eclipse/ui/jcr/util/package-info.java @@ -0,0 +1,2 @@ +/** Generic SWT/JFace JCR helpers. */ +package org.argeo.eclipse.ui.jcr.util; \ No newline at end of file diff --git a/org.argeo.cms.jcr/.classpath b/org.argeo.cms.jcr/.classpath new file mode 100644 index 0000000..3628e33 --- /dev/null +++ b/org.argeo.cms.jcr/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/org.argeo.cms.jcr/.project b/org.argeo.cms.jcr/.project new file mode 100644 index 0000000..3e470f8 --- /dev/null +++ b/org.argeo.cms.jcr/.project @@ -0,0 +1,33 @@ + + + org.argeo.cms.jcr + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/org.argeo.cms.jcr/.settings/org.eclipse.jdt.core.prefs b/org.argeo.cms.jcr/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..7e2e119 --- /dev/null +++ b/org.argeo.cms.jcr/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,101 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning diff --git a/org.argeo.cms.jcr/OSGI-INF/dataServletContext.xml b/org.argeo.cms.jcr/OSGI-INF/dataServletContext.xml new file mode 100644 index 0000000..f5fc8de --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/dataServletContext.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.argeo.cms.jcr/OSGI-INF/filesServlet.xml b/org.argeo.cms.jcr/OSGI-INF/filesServlet.xml new file mode 100644 index 0000000..a283ef0 --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/filesServlet.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/org.argeo.cms.jcr/OSGI-INF/filesServletContext.xml b/org.argeo.cms.jcr/OSGI-INF/filesServletContext.xml new file mode 100644 index 0000000..5fb56e3 --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/filesServletContext.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.argeo.cms.jcr/OSGI-INF/jcrContentProvider.xml b/org.argeo.cms.jcr/OSGI-INF/jcrContentProvider.xml new file mode 100644 index 0000000..47d724a --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/jcrContentProvider.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/org.argeo.cms.jcr/OSGI-INF/jcrDeployment.xml b/org.argeo.cms.jcr/OSGI-INF/jcrDeployment.xml new file mode 100644 index 0000000..033ddbd --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/jcrDeployment.xml @@ -0,0 +1,4 @@ + + + + diff --git a/org.argeo.cms.jcr/OSGI-INF/jcrFsProvider.xml b/org.argeo.cms.jcr/OSGI-INF/jcrFsProvider.xml new file mode 100644 index 0000000..e26453b --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/jcrFsProvider.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/org.argeo.cms.jcr/OSGI-INF/jcrRepositoryFactory.xml b/org.argeo.cms.jcr/OSGI-INF/jcrRepositoryFactory.xml new file mode 100644 index 0000000..b43b519 --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/jcrRepositoryFactory.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/org.argeo.cms.jcr/OSGI-INF/jcrServletContext.xml b/org.argeo.cms.jcr/OSGI-INF/jcrServletContext.xml new file mode 100644 index 0000000..a0885bb --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/jcrServletContext.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/org.argeo.cms.jcr/OSGI-INF/repositoryContextsFactory.xml b/org.argeo.cms.jcr/OSGI-INF/repositoryContextsFactory.xml new file mode 100644 index 0000000..2d06aca --- /dev/null +++ b/org.argeo.cms.jcr/OSGI-INF/repositoryContextsFactory.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/org.argeo.cms.jcr/bnd.bnd b/org.argeo.cms.jcr/bnd.bnd new file mode 100644 index 0000000..01446c1 --- /dev/null +++ b/org.argeo.cms.jcr/bnd.bnd @@ -0,0 +1,40 @@ +Bundle-Activator: org.argeo.cms.jcr.internal.osgi.CmsJcrActivator + +Provide-Capability:\ +cms.datamodel; name=jcrx; cnd=/org/argeo/jcr/jcrx.cnd; abstract=true,\ +cms.datamodel; name=argeo; cnd=/org/argeo/cms/jcr/argeo.cnd; abstract=true,\ +cms.datamodel;name=ldap; cnd=/org/argeo/cms/jcr/ldap.cnd; abstract=true,\ +osgi.service;objectClass="javax.jcr.Repository" + +Import-Package:\ +org.argeo.cms.servlet,\ +javax.jcr.security,\ +org.h2;resolution:=optional;version="[1,3)",\ +org.postgresql;version="[42,43)";resolution:=optional,\ +org.apache.commons.httpclient.cookie;resolution:=optional,\ +org.osgi.framework.namespace;version=0.0.0,\ +org.osgi.*;version=0.0.0,\ +org.osgi.service.http.whiteboard,\ +org.apache.jackrabbit.api.stats;version="[1,4)",\ +org.apache.jackrabbit.api;version="[1,4)",\ +org.apache.jackrabbit.commons;version="[1,4)",\ +org.apache.jackrabbit.spi;version="[1,4)",\ +org.apache.jackrabbit.spi2dav;version="[1,4)",\ +org.apache.jackrabbit.spi2davex;version="[1,4)",\ +org.apache.jackrabbit.webdav.jcr;version="[1,4)",\ +org.apache.jackrabbit.webdav.server;version="[1,4)",\ +org.apache.jackrabbit.webdav.simple;version="[1,4)",\ +org.apache.jackrabbit.*;version="[1,4)",\ +junit.*;resolution:=optional,\ +javax.servlet.*;version="[3,5)",\ +* + +Service-Component:\ +OSGI-INF/repositoryContextsFactory.xml,\ +OSGI-INF/jcrRepositoryFactory.xml,\ +OSGI-INF/jcrFsProvider.xml,\ +OSGI-INF/jcrDeployment.xml,\ +OSGI-INF/jcrServletContext.xml,\ +OSGI-INF/dataServletContext.xml,\ +OSGI-INF/filesServletContext.xml,\ +OSGI-INF/filesServlet.xml,\ diff --git a/org.argeo.cms.jcr/build.properties b/org.argeo.cms.jcr/build.properties new file mode 100644 index 0000000..859c241 --- /dev/null +++ b/org.argeo.cms.jcr/build.properties @@ -0,0 +1,8 @@ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + OSGI-INF/,\ + OSGI-INF/jcrContentProvider.xml +source.. = src/ +additional.bundles = org.apache.jackrabbit.data,\ + org.apache.jackrabbit.spi.commons diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/fs/CmsFsUtils.java b/org.argeo.cms.jcr/src/org/argeo/cms/fs/CmsFsUtils.java new file mode 100644 index 0000000..40d38ee --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/fs/CmsFsUtils.java @@ -0,0 +1,88 @@ +package org.argeo.cms.fs; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.Path; +import java.nio.file.spi.FileSystemProvider; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.query.Query; +import javax.jcr.query.QueryManager; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.jcr.Jcr; + +/** Utilities around documents. */ +public class CmsFsUtils { + // TODO make it more robust and configurable + private static String baseWorkspaceName = CmsConstants.SYS_WORKSPACE; + + public static Node getNode(Repository repository, Path path) { + String workspaceName = path.getNameCount() == 0 ? baseWorkspaceName : path.getName(0).toString(); + String jcrPath = '/' + path.subpath(1, path.getNameCount()).toString(); + try { + Session newSession; + try { + newSession = repository.login(workspaceName); + } catch (NoSuchWorkspaceException e) { + // base workspace + newSession = repository.login(baseWorkspaceName); + jcrPath = path.toString(); + } + return newSession.getNode(jcrPath); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get node from path " + path, e); + } + } + + public static NodeIterator getLastUpdatedDocuments(Session session) { + try { + String qStr = "//element(*, nt:file)"; + qStr += " order by @jcr:lastModified descending"; + QueryManager queryManager = session.getWorkspace().getQueryManager(); + @SuppressWarnings("deprecation") + Query xpathQuery = queryManager.createQuery(qStr, Query.XPATH); + xpathQuery.setLimit(8); + NodeIterator nit = xpathQuery.execute().getNodes(); + return nit; + } catch (RepositoryException e) { + throw new IllegalStateException("Unable to retrieve last updated documents", e); + } + } + + public static Path getPath(FileSystemProvider nodeFileSystemProvider, URI uri) { + try { + FileSystem fileSystem = nodeFileSystemProvider.getFileSystem(uri); + if (fileSystem == null) + fileSystem = nodeFileSystemProvider.newFileSystem(uri, null); + String path = uri.getPath(); + return fileSystem.getPath(path); + } catch (IOException e) { + throw new IllegalStateException("Unable to initialise file system for " + uri, e); + } + } + + public static Path getPath(FileSystemProvider nodeFileSystemProvider, Node node) { + String workspaceName = Jcr.getWorkspaceName(node); + String fullPath = baseWorkspaceName.equals(workspaceName) ? Jcr.getPath(node) + : '/' + workspaceName + Jcr.getPath(node); + URI uri; + try { + uri = new URI(CmsConstants.SCHEME_NODE, null, fullPath, null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot interpret " + fullPath + " as an URI", e); + } + return getPath(nodeFileSystemProvider, uri); + } + + /** Singleton. */ + private CmsFsUtils() { + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/CustomRepositoryConfigurationParser.java b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/CustomRepositoryConfigurationParser.java new file mode 100644 index 0000000..c289857 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/CustomRepositoryConfigurationParser.java @@ -0,0 +1,58 @@ +package org.argeo.cms.internal.jcr; + +import java.util.Properties; + +import org.apache.jackrabbit.core.config.BeanConfig; +import org.apache.jackrabbit.core.config.ConfigurationException; +import org.apache.jackrabbit.core.config.RepositoryConfigurationParser; +import org.apache.jackrabbit.core.config.WorkspaceSecurityConfig; +import org.apache.jackrabbit.core.util.db.ConnectionFactory; +import org.w3c.dom.Element; + +/** + * A {@link RepositoryConfigurationParser} providing more flexibility with + * classloaders. + */ +@SuppressWarnings("restriction") +class CustomRepositoryConfigurationParser extends RepositoryConfigurationParser { + private ClassLoader classLoader = null; + + public CustomRepositoryConfigurationParser(Properties variables) { + super(variables); + } + + public CustomRepositoryConfigurationParser(Properties variables, ConnectionFactory connectionFactory) { + super(variables, connectionFactory); + } + + @Override + protected RepositoryConfigurationParser createSubParser(Properties variables) { + Properties props = new Properties(getVariables()); + props.putAll(variables); + CustomRepositoryConfigurationParser subParser = new CustomRepositoryConfigurationParser(props, + connectionFactory); + subParser.setClassLoader(classLoader); + return subParser; + } + + @Override + public WorkspaceSecurityConfig parseWorkspaceSecurityConfig(Element parent) throws ConfigurationException { + WorkspaceSecurityConfig workspaceSecurityConfig = super.parseWorkspaceSecurityConfig(parent); + workspaceSecurityConfig.getAccessControlProviderConfig().setClassLoader(classLoader); + return workspaceSecurityConfig; + } + + @Override + protected BeanConfig parseBeanConfig(Element parent, String name) throws ConfigurationException { + BeanConfig beanConfig = super.parseBeanConfig(parent, name); + if (beanConfig.getClassName().startsWith("org.argeo")) { + beanConfig.setClassLoader(classLoader); + } + return beanConfig; + } + + public void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/JackrabbitType.java b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/JackrabbitType.java new file mode 100644 index 0000000..40c83f6 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/JackrabbitType.java @@ -0,0 +1,21 @@ +package org.argeo.cms.internal.jcr; + +/** Pre-defined Jackrabbit repository configurations. */ +enum JackrabbitType { + /** Local file system */ + localfs, + /** Embedded Java H2 database */ + h2, + /** Embedded Java H2 database in PostgreSQL compatibility mode */ + h2_postgresql, + /** 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.jcr/src/org/argeo/cms/internal/jcr/LocalFsDataStore.java b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/LocalFsDataStore.java new file mode 100644 index 0000000..dba005c --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/LocalFsDataStore.java @@ -0,0 +1,68 @@ +package org.argeo.cms.internal.jcr; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.apache.jackrabbit.core.data.DataIdentifier; +import org.apache.jackrabbit.core.data.DataRecord; +import org.apache.jackrabbit.core.data.DataStoreException; +import org.apache.jackrabbit.core.data.FileDataStore; + +/** + * experimental Duplicate added entries in another directory (typically a + * remote mount). + */ +@SuppressWarnings("restriction") +public class LocalFsDataStore extends FileDataStore { + String redundantPath; + FileDataStore redundantStore; + + @Override + public void init(String homeDir) { + // init primary first + super.init(homeDir); + + if (redundantPath != null) { + // redundant directory must be created first + // TODO implement some polling? + if (Files.exists(Paths.get(redundantPath))) { + redundantStore = new FileDataStore(); + redundantStore.setPath(redundantPath); + redundantStore.init(homeDir); + } + } + } + + @Override + public DataRecord addRecord(InputStream input) throws DataStoreException { + DataRecord dataRecord = super.addRecord(input); + syncRedundantRecord(dataRecord); + return dataRecord; + } + + @Override + public DataRecord getRecord(DataIdentifier identifier) throws DataStoreException { + DataRecord dataRecord = super.getRecord(identifier); + syncRedundantRecord(dataRecord); + return dataRecord; + } + + protected void syncRedundantRecord(DataRecord dataRecord) throws DataStoreException { + if (redundantStore == null) + return; + if (redundantStore.getRecordIfStored(dataRecord.getIdentifier()) == null) { + try (InputStream redundant = dataRecord.getStream()) { + redundantStore.addRecord(redundant); + } catch (IOException e) { + throw new DataStoreException("Cannot add redundant record.", e); + } + } + } + + public void setRedundantPath(String redundantPath) { + this.redundantPath = redundantPath; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/RepoConf.java b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/RepoConf.java new file mode 100644 index 0000000..336ec82 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/RepoConf.java @@ -0,0 +1,55 @@ +package org.argeo.cms.internal.jcr; + +import org.argeo.api.cms.CmsConstants; + +/** JCR repository configuration */ +public enum RepoConf { + /** Repository type */ + type("h2"), + /** Default workspace */ + defaultWorkspace(CmsConstants.SYS_WORKSPACE), + /** 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; + + RepoConf(String oid, Object def) { + this.def = def; + } + + RepoConf(Object def) { + this.def = def; + } + + public Object getDefault() { + return def; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/RepositoryBuilder.java b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/RepositoryBuilder.java new file mode 100644 index 0000000..3db9716 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/RepositoryBuilder.java @@ -0,0 +1,225 @@ +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.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.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.jcr.internal.CmsPaths; +import org.xml.sax.InputSource; + +/** Can interpret properties in order to create an actual JCR repository. */ +public class RepositoryBuilder { + private final static CmsLog log = CmsLog.getLog(RepositoryBuilder.class); + + public RepositoryContext createRepositoryContext(Dictionary properties) + throws RepositoryException, IOException { + 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 properties) throws RepositoryException, IOException { + JackrabbitType type = JackrabbitType.valueOf(prop(properties, RepoConf.type).toString()); + ClassLoader cl = getClass().getClassLoader(); + final String base = "/org/argeo/cms/internal/jcr"; + try (InputStream in = cl.getResourceAsStream(base + "/repository-" + type.name() + ".xml")) { + if (in == null) + throw new IllegalArgumentException("Repository configuration not found"); + InputSource config = new InputSource(in); + Properties jackrabbitVars = getConfigurationProperties(type, properties); + // RepositoryConfig repositoryConfig = RepositoryConfig.create(config, + // jackrabbitVars); + + // custom configuration parser + CustomRepositoryConfigurationParser parser = new CustomRepositoryConfigurationParser(jackrabbitVars); + parser.setClassLoader(cl); + RepositoryConfig repositoryConfig = parser.parseRepositoryConfig(config); + repositoryConfig.init(); + + // set the proper classloaders + repositoryConfig.getSecurityConfig().getSecurityManagerConfig().setClassLoader(cl); + repositoryConfig.getSecurityConfig().getAccessManagerConfig().setClassLoader(cl); +// for (WorkspaceConfig workspaceConfig : repositoryConfig.getWorkspaceConfigs()) { +// workspaceConfig.getSecurityConfig().getAccessControlProviderConfig().setClassLoader(cl); +// } + return repositoryConfig; + } + } + + private Properties getConfigurationProperties(JackrabbitType type, Dictionary properties) { + Properties props = new Properties(); + for (Enumeration 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 IllegalArgumentException("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(CmsConstants.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 IllegalArgumentException("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 RuntimeException("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 h2_postgresql: + dburl = "jdbc:h2:" + homePath.toAbsolutePath() + "/h2/repository;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE"; + 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 IllegalArgumentException("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 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.isDebugEnabled()) + log.debug( + "Created Jackrabbit repository in " + duration + " s, home: " + repositoryConfig.getHomeDir()); + + return repositoryContext; + } finally { + Thread.currentThread().setContextClassLoader(currentContextCl); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-h2.xml b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-h2.xml new file mode 100644 index 0000000..ace0fa5 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-h2.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-h2_postgresql.xml b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-h2_postgresql.xml new file mode 100644 index 0000000..4303676 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-h2_postgresql.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-localfs.xml b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-localfs.xml new file mode 100644 index 0000000..b889079 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-localfs.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-memory.xml b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-memory.xml new file mode 100644 index 0000000..3630a14 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-memory.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql.xml b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql.xml new file mode 100644 index 0000000..de2f245 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster.xml b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster.xml new file mode 100644 index 0000000..488ad6b --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster_ds.xml b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster_ds.xml new file mode 100644 index 0000000..b430674 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_cluster_ds.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_ds.xml b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_ds.xml new file mode 100644 index 0000000..5229d16 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/internal/jcr/repository-postgresql_ds.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/CmsJcrUtils.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/CmsJcrUtils.java new file mode 100644 index 0000000..7fde177 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/CmsJcrUtils.java @@ -0,0 +1,278 @@ +package org.argeo.cms.jcr; + +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.security.auth.AuthPermission; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.CmsConstants; +import org.argeo.jcr.JcrUtils; +import org.argeo.util.CurrentSubject; + +/** Utilities related to Argeo model in JCR */ +public class CmsJcrUtils { + /** + * Wraps the call to the repository factory based on parameter + * {@link CmsConstants#CN} in order to simplify it and protect against future + * API changes. + */ + public static Repository getRepositoryByAlias(RepositoryFactory repositoryFactory, String alias) { + try { + Map parameters = new HashMap(); + parameters.put(CmsConstants.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 CmsConstants#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 CmsConstants#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 parameters = new HashMap(); + parameters.put(CmsConstants.LABELED_URI, uri); + if (alias != null) + parameters.put(CmsConstants.CN, alias); + return repositoryFactory.getRepository(parameters); + } catch (RepositoryException e) { + throw new RuntimeException("Unexpected exception when trying to retrieve repository with uri " + uri, 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 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); +// } + + try { + checkUserWorkspace(session, username); + String homePath = getHomePath(username); + if (session.itemExists(homePath)) + return session.getNode(homePath); + // legacy + homePath = "/home/" + username; + if (session.itemExists(homePath)) + return session.getNode(homePath); + return null; + } catch (RepositoryException e) { + throw new RuntimeException("Cannot find home for user " + username, e); + } + } + + private static String getHomePath(String username) { + LdapName dn; + try { + dn = new LdapName(username); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Invalid name " + username, e); + } + String userId = dn.getRdn(dn.size() - 1).getValue().toString(); + return '/' + userId; + } + + private static void checkUserWorkspace(Session session, String username) { + String workspaceName = session.getWorkspace().getName(); + if (!CmsConstants.HOME_WORKSPACE.equals(workspaceName)) + throw new IllegalArgumentException(workspaceName + " is not the home workspace for user " + username); + } + + /** + * 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 groupname the name of the group + */ + public static Node getGroupHome(Session session, String groupname) { +// 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 group " + cn, e); +// } + + try { + checkGroupWorkspace(session, groupname); + String homePath = getGroupPath(groupname); + if (session.itemExists(homePath)) + return session.getNode(homePath); + // legacy + homePath = "/groups/" + groupname; + if (session.itemExists(homePath)) + return session.getNode(homePath); + return null; + } catch (RepositoryException e) { + throw new RuntimeException("Cannot find home for group " + groupname, e); + } + + } + + private static String getGroupPath(String groupname) { + String cn; + try { + LdapName dn = new LdapName(groupname); + cn = dn.getRdn(dn.size() - 1).getValue().toString(); + } catch (InvalidNameException e) { + cn = groupname; + } + return '/' + cn; + } + + private static void checkGroupWorkspace(Session session, String groupname) { + String workspaceName = session.getWorkspace().getName(); + if (!CmsConstants.SRV_WORKSPACE.equals(workspaceName)) + throw new IllegalArgumentException(workspaceName + " is not the group workspace for group " + groupname); + } + + /** + * 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); + } + + /** Whether this node is the home of the user of the underlying session. */ + public static boolean isUserHome(Node node) { + try { + String userID = node.getSession().getUserID(); + return node.hasProperty(Property.JCR_ID) && node.getProperty(Property.JCR_ID).getString().equals(userID); + } catch (RepositoryException e) { + throw new IllegalStateException(e); + } + } + + /** + * Translate the path to this node into a path containing the name of the + * repository and the name of the workspace. + */ + public static String getDataPath(String cn, Node node) { + assert node != null; + StringBuilder buf = new StringBuilder(CmsConstants.PATH_DATA); + try { + return buf.append('/').append(cn).append('/').append(node.getSession().getWorkspace().getName()) + .append(node.getPath()).toString(); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get data path for " + node + " in repository " + cn, e); + } + } + + /** + * Translate the path to this node into a path containing the name of the + * repository and the name of the workspace. + */ + public static String getDataPath(Node node) { + return getDataPath(CmsConstants.NODE, node); + } + + /** + * Open a JCR session with full read/write rights on the data, as + * {@link CmsConstants#ROLE_USER_ADMIN}, using the + * {@link CmsAuth#LOGIN_CONTEXT_DATA_ADMIN} login context. For security hardened + * deployement, use {@link AuthPermission} on this login context. + */ + public static Session openDataAdminSession(Repository repository, String workspaceName) { + LoginContext loginContext; + try { + loginContext = CmsAuth.DATA_ADMIN.newLoginContext(); + loginContext.login(); + } catch (LoginException e1) { + throw new RuntimeException("Could not login as data admin", e1); + } finally { + } + + ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(CmsJcrUtils.class.getClassLoader()); + return CurrentSubject.callAs(loginContext.getSubject(), () -> { + try { + return JcrUtils.loginOrCreateWorkspace(repository, workspaceName); + } catch (NoSuchWorkspaceException e) {// should not happen + throw new IllegalArgumentException("No workspace " + workspaceName + " available", e); + } catch (RepositoryException e) { + throw new RuntimeException("Cannot open data admin session", e); + } + } + + ); + } finally { + Thread.currentThread().setContextClassLoader(currentCl); + } + } + + /** Singleton. */ + private CmsJcrUtils() { + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java new file mode 100644 index 0000000..a4af35b --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java @@ -0,0 +1,387 @@ +package org.argeo.cms.jcr.acr; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ForkJoinPool; + +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 javax.jcr.nodetype.NodeType; +import javax.xml.namespace.QName; +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.NamespaceUtils; +import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.acr.AbstractContent; +import org.argeo.cms.acr.ContentUtils; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; + +/** A JCR {@link Node} accessed as {@link Content}. */ +public class JcrContent extends AbstractContent { +// private Node jcrNode; + + private JcrContentProvider provider; + + private String jcrWorkspace; + private String jcrPath; + + protected JcrContent(ProvidedSession session, JcrContentProvider provider, String jcrWorkspace, String jcrPath) { + super(session); + this.provider = provider; + this.jcrWorkspace = jcrWorkspace; + this.jcrPath = jcrPath; + } + + @Override + public QName getName() { + String name = Jcr.getName(getJcrNode()); + if (name.equals("")) {// root + String mountPath = provider.getMountPath(); + name = ContentUtils.getParentPath(mountPath)[1]; + // name = Jcr.getWorkspaceName(getJcrNode()); + } + return NamespaceUtils.parsePrefixedName(provider, name); + } + + @SuppressWarnings("unchecked") + @Override + public Optional get(QName key, Class clss) { + if (isDefaultAttrTypeRequested(clss)) { + return Optional.of((A) get(getJcrNode(), key.toString())); + } + return Optional.of((A) Jcr.get(getJcrNode(), key.toString())); + } + + @Override + public Iterator iterator() { + try { + return new JcrContentIterator(getJcrNode().getNodes()); + } catch (RepositoryException e) { + throw new JcrException("Cannot list children of " + getJcrNode(), e); + } + } + + @Override + protected Iterable keys() { + try { + Set keys = new HashSet<>(); + for (PropertyIterator propertyIterator = getJcrNode().getProperties(); propertyIterator.hasNext();) { + Property property = propertyIterator.nextProperty(); + // TODO convert standard names + // TODO skip technical properties + QName name = NamespaceUtils.parsePrefixedName(provider, property.getName()); + keys.add(name); + } + return keys; + } catch (RepositoryException e) { + throw new JcrException("Cannot list properties of " + getJcrNode(), e); + } + } + + public Node getJcrNode() { + try { + // TODO caching? + return provider.getJcrSession(getSession(), jcrWorkspace).getNode(jcrPath); + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve " + jcrPath + " from workspace " + jcrWorkspace, e); + } + } + + /** Cast to a standard Java object. */ + static Object get(Node node, String property) { + try { + Property p = node.getProperty(property); + if (p.isMultiple()) { + Value[] values = p.getValues(); + List lst = new ArrayList<>(); + for (Value value : values) { + lst.add(convertSingleValue(value)); + } + return lst; + } else { + Value value = node.getProperty(property).getValue(); + return convertSingleValue(value); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot cast value from " + property + " of node " + node, e); + } + } + + static Object convertSingleValue(Value value) throws RepositoryException { + switch (value.getType()) { + case PropertyType.STRING: + return value.getString(); + case PropertyType.DOUBLE: + return (Double) value.getDouble(); + case PropertyType.LONG: + return (Long) value.getLong(); + case PropertyType.BOOLEAN: + return (Boolean) value.getBoolean(); + case PropertyType.DATE: + Calendar calendar = value.getDate(); + return calendar.toInstant(); + case PropertyType.BINARY: + throw new IllegalArgumentException("Binary is not supported as an attribute"); + default: + return value.getString(); + } + } + + class JcrContentIterator implements Iterator { + private final NodeIterator nodeIterator; + // we keep track in order to be able to delete it + private JcrContent current = null; + + protected JcrContentIterator(NodeIterator nodeIterator) { + this.nodeIterator = nodeIterator; + } + + @Override + public boolean hasNext() { + return nodeIterator.hasNext(); + } + + @Override + public Content next() { + current = new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getPath(nodeIterator.nextNode())); + return current; + } + + @Override + public void remove() { + if (current != null) { + Jcr.remove(current.getJcrNode()); + } + } + + } + + @Override + public String getPath() { + try { + // Note: it is important to to use the default way (recursing through parents), + // since the session may not have access to parent nodes + return ContentUtils.ROOT_SLASH + jcrWorkspace + getJcrNode().getPath(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get depth of " + getJcrNode(), e); + } + } + + @Override + public int getDepth() { + try { + return getJcrNode().getDepth() + 1; + } catch (RepositoryException e) { + throw new JcrException("Cannot get depth of " + getJcrNode(), e); + } + } + + @Override + public Content getParent() { + if (Jcr.isRoot(getJcrNode())) // root + return null; + return new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getParentPath(getJcrNode())); + } + + @Override + public Content add(QName name, QName... classes) { + if (classes.length > 0) { + QName primaryType = classes[0]; + Node child = Jcr.addNode(getJcrNode(), name.toString(), primaryType.toString()); + for (int i = 1; i < classes.length; i++) { + try { + child.addMixin(classes[i].toString()); + } catch (RepositoryException e) { + throw new JcrException("Cannot add child to " + getJcrNode(), e); + } + } + + } else { + Jcr.addNode(getJcrNode(), name.toString(), NodeType.NT_UNSTRUCTURED); + } + return null; + } + + @Override + public void remove() { + Jcr.remove(getJcrNode()); + } + + @Override + protected void removeAttr(QName key) { + Property property = Jcr.getProperty(getJcrNode(), key.toString()); + if (property != null) { + try { + property.remove(); + } catch (RepositoryException e) { + throw new JcrException("Cannot remove property " + key + " from " + getJcrNode(), e); + } + } + + } + + boolean exists() { + try { + return provider.getJcrSession(getSession(), jcrWorkspace).itemExists(jcrPath); + } catch (RepositoryException e) { + throw new JcrException("Cannot check whether " + jcrPath + " exists", e); + } + } + + /* + * ADAPTERS + */ + @SuppressWarnings("unchecked") + public A adapt(Class clss) { + if (Source.class.isAssignableFrom(clss)) { +// try { + PipedInputStream in = new PipedInputStream(); + + ForkJoinPool.commonPool().execute(() -> { + try (PipedOutputStream out = new PipedOutputStream(in)) { + provider.getJcrSession(getSession(), jcrWorkspace).exportDocumentView(jcrPath, out, true, false); + out.flush(); + } catch (IOException | RepositoryException e) { + throw new RuntimeException("Cannot export " + jcrPath + " in workspace " + jcrWorkspace, e); + } + + }); + return (A) new StreamSource(in); +// } catch (IOException e) { +// throw new RuntimeException("Cannot adapt " + JcrContent.this + " to " + clss, e); +// } + } else + + return super.adapt(clss); + } + + @SuppressWarnings("unchecked") + @Override + public C open(Class clss) throws IOException, IllegalArgumentException { + if (InputStream.class.isAssignableFrom(clss)) { + Node node = getJcrNode(); + if (Jcr.isNodeType(node, NodeType.NT_FILE)) { + try { + return (C) JcrUtils.getFileAsStream(node); + } catch (RepositoryException e) { + throw new JcrException("Cannot open " + jcrPath + " in workspace " + jcrWorkspace, e); + } + } + } + return super.open(clss); + } + + @Override + public ContentProvider getProvider() { + return provider; + } + + @Override + public String getSessionLocalId() { + try { + return getJcrNode().getIdentifier(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get identifier for " + getJcrNode(), e); + } + } + + /* + * TYPING + */ + @Override + public List getContentClasses() { + try { +// Node node = getJcrNode(); +// List res = new ArrayList<>(); +// res.add(nodeTypeToQName(node.getPrimaryNodeType())); +// for (NodeType mixin : node.getMixinNodeTypes()) { +// res.add(nodeTypeToQName(mixin)); +// } +// return res; + Node context = getJcrNode(); + + List res = new ArrayList<>(); + // primary node type + NodeType primaryType = context.getPrimaryNodeType(); + res.add(nodeTypeToQName(primaryType)); + + Set secondaryTypes = new TreeSet<>(NamespaceUtils.QNAME_COMPARATOR); + for (NodeType mixinType : context.getMixinNodeTypes()) { + secondaryTypes.add(nodeTypeToQName(mixinType)); + } + for (NodeType superType : primaryType.getDeclaredSupertypes()) { + secondaryTypes.add(nodeTypeToQName(superType)); + } + // mixins + for (NodeType mixinType : context.getMixinNodeTypes()) { + for (NodeType superType : mixinType.getDeclaredSupertypes()) { + secondaryTypes.add(nodeTypeToQName(superType)); + } + } +// // entity type +// if (context.isNodeType(EntityType.entity.get())) { +// if (context.hasProperty(EntityNames.ENTITY_TYPE)) { +// String entityTypeName = context.getProperty(EntityNames.ENTITY_TYPE).getString(); +// if (byType.containsKey(entityTypeName)) { +// types.add(entityTypeName); +// } +// } +// } + res.addAll(secondaryTypes); + return res; + } catch (RepositoryException e) { + throw new JcrException("Cannot list node types from " + getJcrNode(), e); + } + } + + private QName nodeTypeToQName(NodeType nodeType) { + String name = nodeType.getName(); + return QName.valueOf(name); + } + + @Override + public int getSiblingIndex() { + return Jcr.getIndex(getJcrNode()); + } + + /* + * STATIC UTLITIES + */ + public static Content nodeToContent(Node node) { + if (node == null) + return null; + try { + ProvidedSession contentSession = (ProvidedSession) node.getSession() + .getAttribute(ProvidedSession.class.getName()); + if (contentSession == null) + throw new IllegalArgumentException( + "Cannot adapt " + node + " to content, because it was not loaded from a content session"); + return contentSession.get(ContentUtils.SLASH + CmsConstants.SYS_WORKSPACE + node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot adapt " + node + " to a content", e); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java new file mode 100644 index 0000000..eaa27b7 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentProvider.java @@ -0,0 +1,122 @@ +package org.argeo.cms.jcr.acr; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.xml.namespace.NamespaceContext; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.api.acr.spi.ProvidedContent; +import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.acr.ContentUtils; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; + +/** A JCR workspace accessed as an {@link ContentProvider}. */ +public class JcrContentProvider implements ContentProvider, NamespaceContext { + + private Repository jcrRepository; + private Session adminSession; + + private String mountPath; + + // cache + private String jcrWorkspace; + + private Map sessionAdapters = Collections.synchronizedMap(new HashMap<>()); + + public void start(Map properties) { + mountPath = properties.get(CmsConstants.ACR_MOUNT_PATH); + if ("/".equals(mountPath)) + throw new IllegalArgumentException("JCR content provider cannot be root /"); + Objects.requireNonNull(mountPath); + jcrWorkspace = ContentUtils.getParentPath(mountPath)[1]; + adminSession = CmsJcrUtils.openDataAdminSession(jcrRepository, jcrWorkspace); + } + + public void stop() { + if (adminSession.isLive()) + JcrUtils.logoutQuietly(adminSession); + } + + public void setJcrRepository(Repository jcrRepository) { + this.jcrRepository = jcrRepository; + } + + @Override + public ProvidedContent get(ProvidedSession contentSession, String relativePath) { + String jcrPath = "/" + relativePath; + return new JcrContent(contentSession, this, jcrWorkspace, jcrPath); + } + + @Override + public boolean exists(ProvidedSession contentSession, String relativePath) { + String jcrWorkspace = ContentUtils.getParentPath(mountPath)[1]; + String jcrPath = "/" + relativePath; + return new JcrContent(contentSession, this, jcrWorkspace, jcrPath).exists(); + } + + public Session getJcrSession(ProvidedSession contentSession, String jcrWorkspace) { + JcrSessionAdapter sessionAdapter = sessionAdapters.get(contentSession); + if (sessionAdapter == null) { + final JcrSessionAdapter newSessionAdapter = new JcrSessionAdapter(jcrRepository, contentSession, + contentSession.getSubject()); + sessionAdapters.put(contentSession, newSessionAdapter); + contentSession.onClose().thenAccept((s) -> newSessionAdapter.close()); + sessionAdapter = newSessionAdapter; + } + + Session jcrSession = sessionAdapter.getSession(jcrWorkspace); + return jcrSession; + } + + public Session getJcrSession(Content content, String jcrWorkspace) { + return getJcrSession(((ProvidedContent) content).getSession(), jcrWorkspace); + } + + @Override + public String getMountPath() { + return mountPath; + } + + /* + * NAMESPACE CONTEXT + */ + @Override + public String getNamespaceURI(String prefix) { + try { + return adminSession.getNamespaceURI(prefix); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + @Override + public String getPrefix(String namespaceURI) { + try { + return adminSession.getNamespacePrefix(namespaceURI); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + try { + return Arrays.asList(adminSession.getNamespacePrefix(namespaceURI)).iterator(); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentUtils.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentUtils.java new file mode 100644 index 0000000..25e54b3 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContentUtils.java @@ -0,0 +1,261 @@ +package org.argeo.cms.jcr.acr; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.xml.XMLConstants; +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.ContentName; +import org.argeo.api.acr.CrName; +import org.argeo.api.acr.DName; +import org.argeo.api.acr.NamespaceUtils; +import org.argeo.api.acr.spi.ProvidedContent; +import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.api.cms.CmsLog; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** Utilities around integration between JCR and ACR. */ +public class JcrContentUtils { + private final static CmsLog log = CmsLog.getLog(JcrContentUtils.class); + + public static void copyFiles(Node folder, Content collection, String... additionalCollectionTypes) { + try { + log.debug("Copy collection " + collection); + + NamespaceContext jcrNamespaceContext = new JcrSessionNamespaceContext(folder.getSession()); + + nodes: for (NodeIterator it = folder.getNodes(); it.hasNext();) { + Node node = it.nextNode(); + String name = node.getName(); + if (node.isNodeType(NodeType.NT_FILE)) { + Content file = collection.anyOrAddChild(new ContentName(name)); + try (InputStream in = JcrUtils.getFileAsStream(node)) { + file.write(InputStream.class).complete(in); + } + } else if (node.isNodeType(NodeType.NT_FOLDER)) { + Content subCol = collection.add(name, DName.collection.qName()); + copyFiles(node, subCol, additionalCollectionTypes); + } else { + List contentClasses = typesAsContentClasses(node, jcrNamespaceContext); + for (String collectionType : additionalCollectionTypes) { + if (node.isNodeType(collectionType)) { + contentClasses.add(DName.collection.qName()); + Content subCol = collection.add(name, + contentClasses.toArray(new QName[contentClasses.size()])); + setAttributes(node, subCol, jcrNamespaceContext); +// setContentClasses(node, subCol, jcrNamespaceContext); + copyFiles(node, subCol, additionalCollectionTypes); + continue nodes; + } + } + + QName qName = NamespaceUtils.parsePrefixedName(name); +// if (NamespaceUtils.hasNamespace(qName)) { + if (node.getIndex() > 1) { + log.warn("Same name siblings not supported, skipping " + node); + continue nodes; + } + Content content = collection.add(qName, contentClasses.toArray(new QName[contentClasses.size()])); + Source source = toSource(node); + ((ProvidedContent) content).getSession().edit((s) -> { + ((ProvidedSession) s).notifyModification((ProvidedContent) content); + content.write(Source.class).complete(source); +// try { +// //setContentClasses(node, content, jcrNamespaceContext); +// } catch (RepositoryException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } + }).toCompletableFuture().join(); + setAttributes(node, content, jcrNamespaceContext); + +// } else { +// // ignore +// log.debug(() -> "Ignored " + node); +// continue nodes; +// } + } + } + } catch (RepositoryException e) { + throw new JcrException("Cannot copy files from " + folder + " to " + collection, e); + } catch (IOException e) { + throw new RuntimeException("Cannot copy files from " + folder + " to " + collection, e); + } + } + + private static Source toSource(Node node) throws RepositoryException { + +// try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { +// node.getSession().exportDocumentView(node.getPath(), out, true, false); +// System.out.println(new String(out.toByteArray(), StandardCharsets.UTF_8)); +//// DocumentBuilder documentBuilder = DocumentBuilderFactory.newNSInstance().newDocumentBuilder(); +//// Document document; +//// try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray())) { +//// document = documentBuilder.parse(in); +//// } +//// cleanJcrDom(document); +//// return new DOMSource(document); +// } catch (IOException e) { +// throw new RuntimeException(e); +// } + + try (PipedInputStream in = new PipedInputStream();) { + + CompletableFuture toDo = CompletableFuture.supplyAsync(() -> { + try { + DocumentBuilder documentBuilder = DocumentBuilderFactory.newNSInstance().newDocumentBuilder(); + return documentBuilder.parse(in); + } catch (ParserConfigurationException | SAXException | IOException e) { + throw new RuntimeException("Cannot parse", e); + } + }); + + // TODO optimise + try (PipedOutputStream out = new PipedOutputStream(in)) { + node.getSession().exportDocumentView(node.getPath(), out, true, false); + } catch (IOException | RepositoryException e) { + throw new RuntimeException("Cannot export " + node + " in workspace " + Jcr.getWorkspaceName(node), e); + } + Document document = toDo.get(); + cleanJcrDom(document); + return new DOMSource(document); + } catch (IOException | InterruptedException | ExecutionException e1) { + throw new RuntimeException("Cannot parse", e1); + } + + } + + public static void setAttributes(Node source, Content target, NamespaceContext jcrNamespaceContext) + throws RepositoryException { + properties: for (PropertyIterator pit = source.getProperties(); pit.hasNext();) { + Property p = pit.nextProperty(); + // TODO migrate JCR title, last modified, etc. ? + if (p.getName().startsWith("jcr:")) + continue properties; + if (p.isMultiple()) { + List attr = new ArrayList<>(); + for (Value value : p.getValues()) { + attr.add(value.getString()); + } + target.put(NamespaceUtils.parsePrefixedName(jcrNamespaceContext, p.getName()), attr); + } else { + target.put(NamespaceUtils.parsePrefixedName(jcrNamespaceContext, p.getName()), p.getString()); + } + } + } + + public static List typesAsContentClasses(Node source, NamespaceContext jcrNamespaceContext) + throws RepositoryException { + // TODO super types? + List contentClasses = new ArrayList<>(); + contentClasses + .add(NamespaceUtils.parsePrefixedName(jcrNamespaceContext, source.getPrimaryNodeType().getName())); + for (NodeType nodeType : source.getMixinNodeTypes()) { + contentClasses.add(NamespaceUtils.parsePrefixedName(jcrNamespaceContext, nodeType.getName())); + } + // filter out JCR types + for (Iterator it = contentClasses.iterator(); it.hasNext();) { + QName type = it.next(); + if (type.getNamespaceURI().equals(JCR_NT_NAMESPACE_URI) + || type.getNamespaceURI().equals(JCR_MIX_NAMESPACE_URI)) { + it.remove(); + } + } + // target.addContentClasses(contentClasses.toArray(new + // QName[contentClasses.size()])); + return contentClasses; + } + + static final String JCR_NAMESPACE_URI = "http://www.jcp.org/jcr/1.0"; + static final String JCR_NT_NAMESPACE_URI = "http://www.jcp.org/jcr/nt/1.0"; + static final String JCR_MIX_NAMESPACE_URI = "http://www.jcp.org/jcr/mix/1.0"; + + public static void cleanJcrDom(Document document) { + Element documentElement = document.getDocumentElement(); + Set namespaceUris = new HashSet<>(); + cleanJcrDom(documentElement, namespaceUris); + + // remove unused namespaces + NamedNodeMap attrs = documentElement.getAttributes(); + Set toRemove = new HashSet<>(); + for (int i = 0; i < attrs.getLength(); i++) { + Attr attr = (Attr) attrs.item(i); +// log.debug("Check "+i+" " + attr); + String prefix = attr.getPrefix(); + if (prefix != null && prefix.equals(XMLConstants.XMLNS_ATTRIBUTE)) { + String namespaceUri = attr.getValue(); + if (!namespaceUris.contains(namespaceUri)) { + toRemove.add(attr); + // log.debug("Removing "+i+" " + namespaceUri); + } + } + } + for (Attr attr : toRemove) + documentElement.removeAttributeNode(attr); + + } + + private static void cleanJcrDom(Element element, Set namespaceUris) { + NodeList children = element.getElementsByTagName("*"); + for (int i = 0; i < children.getLength(); i++) { + Element child = (Element) children.item(i); + if (!namespaceUris.contains(child.getNamespaceURI())) + namespaceUris.add(child.getNamespaceURI()); + cleanJcrDom(child, namespaceUris); + } + + NamedNodeMap attrs = element.getAttributes(); + attributes: for (int i = 0; i < attrs.getLength(); i++) { + Attr attr = (Attr) attrs.item(i); + String namespaceUri = attr.getNamespaceURI(); + if (namespaceUri == null) + continue attributes; + if (JCR_NAMESPACE_URI.equals(namespaceUri)) { + // FIXME probably wrong to change attributes length + element.removeAttributeNode(attr); + continue attributes; + } + if (!namespaceUris.contains(namespaceUri)) + namespaceUris.add(attr.getNamespaceURI()); + + } + + } + + /** singleton */ + private JcrContentUtils() { + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionAdapter.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionAdapter.java new file mode 100644 index 0000000..ae8ae80 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionAdapter.java @@ -0,0 +1,91 @@ +package org.argeo.cms.jcr.acr; + +import java.security.PrivilegedAction; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.security.auth.Subject; + +import org.apache.jackrabbit.core.SessionImpl; +import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; + +/** Manages JCR {@link Session} in an ACR context. */ +class JcrSessionAdapter { + private Repository repository; + private Subject subject; + + private ProvidedSession contentSession; + + private Map> threadSessions = Collections.synchronizedMap(new HashMap<>()); + + private boolean closed = false; + + private Thread lastRetrievingThread = null; + + public JcrSessionAdapter(Repository repository, ProvidedSession contentSession, Subject subject) { + this.repository = repository; + this.contentSession = contentSession; + this.subject = subject; + } + + public synchronized void close() { + for (Map sessions : threadSessions.values()) { + for (Session session : sessions.values()) { + JcrUtils.logoutQuietly(session); + } + sessions.clear(); + } + threadSessions.clear(); + closed = true; + } + + public synchronized Session getSession(String workspace) { + if (closed) + throw new IllegalStateException("JCR session adapter is closed."); + + Thread currentThread = Thread.currentThread(); + if (lastRetrievingThread == null) + lastRetrievingThread = currentThread; + + Map threadSession = threadSessions.get(currentThread); + if (threadSession == null) { + threadSession = new HashMap<>(); + threadSessions.put(currentThread, threadSession); + } + + Session session = threadSession.get(workspace); + if (session == null) { + session = Subject.doAs(subject, (PrivilegedAction) () -> { + try { +// String username = CurrentUser.getUsername(subject); +// SimpleCredentials credentials = new SimpleCredentials(username, new char[0]); +// credentials.setAttribute(ProvidedSession.class.getName(), contentSession); + Session sess = repository.login(workspace); + // Jackrabbit specific: + ((SessionImpl)sess).setAttribute(ProvidedSession.class.getName(), contentSession); + return sess; + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot log in to " + workspace, e); + } + }); + threadSession.put(workspace, session); + } + + if (lastRetrievingThread != currentThread) { + try { + session.refresh(true); + } catch (RepositoryException e) { + throw new JcrException("Cannot refresh JCR session " + session, e); + } + } + lastRetrievingThread = currentThread; + return session; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionNamespaceContext.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionNamespaceContext.java new file mode 100644 index 0000000..a55f4a3 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrSessionNamespaceContext.java @@ -0,0 +1,46 @@ +package org.argeo.cms.jcr.acr; + +import java.util.Arrays; +import java.util.Iterator; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.xml.namespace.NamespaceContext; + +import org.argeo.jcr.JcrException; + +/** A {@link NamespaceContext} based on a JCR {@link Session}. */ +public class JcrSessionNamespaceContext implements NamespaceContext { + private final Session session; + + public JcrSessionNamespaceContext(Session session) { + this.session = session; + } + + @Override + public String getNamespaceURI(String prefix) { + try { + return session.getNamespaceURI(prefix); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + @Override + public String getPrefix(String namespaceURI) { + try { + return session.getNamespacePrefix(namespaceURI); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + try { + return Arrays.asList(session.getNamespacePrefix(namespaceURI)).iterator(); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/argeo.cnd b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/argeo.cnd new file mode 100644 index 0000000..c9e6ee7 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/argeo.cnd @@ -0,0 +1,34 @@ + + +// 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] +mixin +// initialization vector used by some algorithms +- argeo:iv (BINARY) + +[argeo:pbeKeySpec] +mixin +- argeo:secretKeyFactory (STRING) +- argeo:salt (BINARY) +- argeo:iterationCount (LONG) +- argeo:keyLength (LONG) +- argeo:secretKeyEncryption (STRING) + +[argeo:pbeSpec] > argeo:pbeKeySpec +mixin +- argeo:cipher (STRING) diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/dn.cnd b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/dn.cnd new file mode 100644 index 0000000..80849be --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/dn.cnd @@ -0,0 +1,10 @@ +// DN (see https://tools.ietf.org/html/rfc4514) + + + + + + + + + diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrDeployment.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrDeployment.java new file mode 100644 index 0000000..35800f8 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrDeployment.java @@ -0,0 +1,475 @@ +package org.argeo.cms.jcr.internal; + +import static org.argeo.cms.osgi.DataModelNamespace.CMS_DATA_MODEL_NAMESPACE; +import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +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.RepositoryException; +import javax.jcr.Session; +import javax.servlet.Servlet; + +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.core.RepositoryContext; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.ArgeoNames; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.jcr.internal.servlet.CmsRemotingServlet; +import org.argeo.cms.jcr.internal.servlet.CmsWebDavServlet; +import org.argeo.cms.jcr.internal.servlet.JcrHttpUtils; +import org.argeo.cms.osgi.DataModelNamespace; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +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.http.whiteboard.HttpWhiteboardConstants; +import org.osgi.util.tracker.ServiceTracker; + +/** Implementation of a CMS deployment. */ +public class CmsJcrDeployment { + private final CmsLog log = CmsLog.getLog(getClass()); + private final BundleContext bc = FrameworkUtil.getBundle(getClass()).getBundleContext(); + + private DataModels dataModels; + private String webDavConfig = JcrHttpUtils.WEBDAV_CONFIG; + + private boolean argeoDataModelExtensionsAvailable = false; + + // Readiness + private boolean nodeAvailable = false; + +// CmsDeployment cmsDeployment; + public void start() { + dataModels = new DataModels(bc); + + ServiceTracker repoContextSt = new RepositoryContextStc(); + repoContextSt.open(); + // KernelUtils.asyncOpen(repoContextSt); + +// nodeDeployment = CmsJcrActivator.getService(NodeDeployment.class); + + //JcrInitUtils.addToDeployment(cmsDeployment); + +// contentRepository.registerTypes(NamespaceRegistry.PREFIX_JCR, NamespaceRegistry.NAMESPACE_JCR, null); +// contentRepository.registerTypes(NamespaceRegistry.PREFIX_MIX, NamespaceRegistry.NAMESPACE_MIX, null); +// contentRepository.registerTypes(NamespaceRegistry.PREFIX_NT, NamespaceRegistry.NAMESPACE_NT, null); +// // Jackrabbit +// // see +// // https://jackrabbit.apache.org/archive/wiki/JCR/NamespaceRegistry_115513459.html +// contentRepository.registerTypes("rep", "internal", null); + + } + + public void stop() { +// if (nodeHttp != null) +// nodeHttp.destroy(); + + + try { + for (ServiceReference sr : bc + .getServiceReferences(JackrabbitLocalRepository.class, null)) { + bc.getService(sr).destroy(); + } + } catch (InvalidSyntaxException e1) { + log.error("Cannot clean repositories", e1); + } + + } + +// public void setCmsDeployment(CmsDeployment cmsDeployment) { +// this.cmsDeployment = cmsDeployment; +// } + + /** + * Checks whether the deployment is available according to expectations, and + * mark it as available. + */ +// private synchronized void checkReadiness() { +// if (isAvailable()) +// return; +// 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); +// } +// } + + private void prepareNodeRepository(Repository deployedNodeRepository, List publishAsLocalRepo) { +// if (availableSince != null) { +// throw new IllegalStateException("Deployment is already available"); +// } + + // home + prepareDataModel(CmsConstants.NODE_REPOSITORY, deployedNodeRepository, publishAsLocalRepo); + + // init from backup +// if (deployConfig.isFirstInit()) { +// Path restorePath = Paths.get(System.getProperty("user.dir"), "restore"); +// if (Files.exists(restorePath)) { +// if (log.isDebugEnabled()) +// log.debug("Found backup " + restorePath + ", restoring it..."); +// LogicalRestore logicalRestore = new LogicalRestore(bc, deployedNodeRepository, restorePath); +// KernelUtils.doAsDataAdmin(logicalRestore); +// log.info("Restored backup from " + restorePath); +// } +// } + + // init from repository +// Collection> initRepositorySr; +// try { +// initRepositorySr = bc.getServiceReferences(Repository.class, +// "(" + CmsConstants.CN + "=" + CmsConstants.NODE_INIT + ")"); +// } catch (InvalidSyntaxException e1) { +// throw new IllegalArgumentException(e1); +// } +// Iterator> it = initRepositorySr.iterator(); +// while (it.hasNext()) { +// ServiceReference sr = it.next(); +// Object labeledUri = sr.getProperties().get(LdapAttrs.labeledURI.name()); +// Repository initRepository = bc.getService(sr); +// if (log.isDebugEnabled()) +// log.debug("Found init repository " + labeledUri + ", copying it..."); +// initFromRepository(deployedNodeRepository, initRepository); +// log.info("Node repository initialised from " + labeledUri); +// } + } + + /** Init from a (typically remote) repository. */ + private void initFromRepository(Repository deployedNodeRepository, Repository initRepository) { + Session initSession = null; + try { + initSession = initRepository.login(); + workspaces: for (String workspaceName : initSession.getWorkspace().getAccessibleWorkspaceNames()) { + if ("security".equals(workspaceName)) + continue workspaces; + if (log.isDebugEnabled()) + log.debug("Copying workspace " + workspaceName + " from init repository..."); + long begin = System.currentTimeMillis(); + Session targetSession = null; + Session sourceSession = null; + try { + try { + targetSession = CmsJcrUtils.openDataAdminSession(deployedNodeRepository, workspaceName); + } catch (IllegalArgumentException e) {// no such workspace + Session adminSession = CmsJcrUtils.openDataAdminSession(deployedNodeRepository, null); + try { + adminSession.getWorkspace().createWorkspace(workspaceName); + } finally { + Jcr.logout(adminSession); + } + targetSession = CmsJcrUtils.openDataAdminSession(deployedNodeRepository, workspaceName); + } + sourceSession = initRepository.login(workspaceName); +// JcrUtils.copyWorkspaceXml(sourceSession, targetSession); + // TODO deal with referenceable nodes + JcrUtils.copy(sourceSession.getRootNode(), targetSession.getRootNode()); + targetSession.save(); + long duration = System.currentTimeMillis() - begin; + if (log.isDebugEnabled()) + log.debug("Copied workspace " + workspaceName + " from init repository in " + (duration / 1000) + + " s"); + } catch (Exception e) { + log.error("Cannot copy workspace " + workspaceName + " from init repository.", e); + } finally { + Jcr.logout(sourceSession); + Jcr.logout(targetSession); + } + } + } catch (RepositoryException e) { + throw new JcrException(e); + } finally { + Jcr.logout(initSession); + } + } + + private void prepareHomeRepository(RepositoryImpl deployedRepository) { + Session adminSession = KernelUtils.openAdminSession(deployedRepository); + try { + argeoDataModelExtensionsAvailable = Arrays + .asList(adminSession.getWorkspace().getNamespaceRegistry().getURIs()) + .contains(ArgeoNames.ARGEO_NAMESPACE); + } catch (RepositoryException e) { + log.warn("Cannot check whether Argeo namespace is registered assuming it isn't.", e); + argeoDataModelExtensionsAvailable = false; + } finally { + JcrUtils.logoutQuietly(adminSession); + } + + // Publish home with the highest service ranking + Hashtable regProps = new Hashtable<>(); + regProps.put(CmsConstants.CN, CmsConstants.EGO_REPOSITORY); + regProps.put(Constants.SERVICE_RANKING, Integer.MAX_VALUE); + Repository egoRepository = new EgoRepository(deployedRepository, false); + bc.registerService(Repository.class, egoRepository, regProps); + registerRepositoryServlets(CmsConstants.EGO_REPOSITORY, egoRepository); + + // Keyring only if Argeo extensions are available +// if (argeoDataModelExtensionsAvailable) { +// new ServiceTracker(bc, CallbackHandler.class, null) { +// +// @Override +// public CallbackHandler addingService(ServiceReference reference) { +// NodeKeyRing nodeKeyring = new NodeKeyRing(egoRepository); +// CallbackHandler callbackHandler = bc.getService(reference); +// nodeKeyring.setDefaultCallbackHandler(callbackHandler); +// bc.registerService(LangUtils.names(Keyring.class, CryptoKeyring.class, ManagedService.class), +// nodeKeyring, LangUtils.dict(Constants.SERVICE_PID, CmsConstants.NODE_KEYRING_PID)); +// return callbackHandler; +// } +// +// }.open(); +// } + } + + /** Session is logged out. */ + private void prepareDataModel(String cn, Repository repository, List publishAsLocalRepo) { + Session adminSession = KernelUtils.openAdminSession(repository); + try { + Set processed = new HashSet(); + bundles: for (Bundle bundle : bc.getBundles()) { + BundleWiring wiring = bundle.adapt(BundleWiring.class); + if (wiring == null) + continue bundles; + if (CmsConstants.NODE_REPOSITORY.equals(cn))// process all data models + processWiring(cn, adminSession, wiring, processed, false, publishAsLocalRepo); + else { + List 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, false, publishAsLocalRepo); + } + } + } + } finally { + JcrUtils.logoutQuietly(adminSession); + } + } + + private void processWiring(String cn, Session adminSession, BundleWiring wiring, Set processed, + boolean importListedAbstractModels, List publishAsLocalRepo) { + // recursively process requirements first + List requiredWires = wiring.getRequiredWires(CMS_DATA_MODEL_NAMESPACE); + for (BundleWire wire : requiredWires) { + processWiring(cn, adminSession, wire.getProviderWiring(), processed, true, publishAsLocalRepo); + } + + List capabilities = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE); + capabilities: for (BundleCapability capability : capabilities) { + if (!importListedAbstractModels + && KernelUtils.asBoolean((String) capability.getAttributes().get(DataModelNamespace.ABSTRACT))) { + continue capabilities; + } + boolean publish = registerDataModelCapability(cn, adminSession, capability, processed); + if (publish) + publishAsLocalRepo.add((String) capability.getAttributes().get(DataModelNamespace.NAME)); + } + } + + private boolean registerDataModelCapability(String cn, Session adminSession, BundleCapability capability, + Set processed) { + Map 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 IllegalArgumentException("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) { + log.error("Cannot import CND " + url, e); + } + } + } + + if (KernelUtils.asBoolean((String) attrs.get(DataModelNamespace.ABSTRACT))) + return false; + // Non abstract + boolean isStandalone = isStandalone(name); + boolean publishLocalRepo; + if (isStandalone && name.equals(cn))// includes the node itself + publishLocalRepo = true; + else if (!isStandalone && cn.equals(CmsConstants.NODE_REPOSITORY)) + publishLocalRepo = true; + else + publishLocalRepo = false; + + return publishLocalRepo; + } + + boolean isStandalone(String dataModelName) { + return true; + //return cmsDeployment.getProps(CmsConstants.NODE_REPOS_FACTORY_PID, dataModelName) != null; + } + + private void publishLocalRepo(String dataModelName, Repository repository) { + Hashtable properties = new Hashtable<>(); + properties.put(CmsConstants.CN, dataModelName); + LocalRepository localRepository; + String[] classes; + if (repository instanceof RepositoryImpl) { + localRepository = new JackrabbitLocalRepository((RepositoryImpl) repository, dataModelName); + classes = new String[] { Repository.class.getName(), LocalRepository.class.getName(), + JackrabbitLocalRepository.class.getName() }; + } else { + localRepository = new LocalRepository(repository, dataModelName); + classes = new String[] { Repository.class.getName(), LocalRepository.class.getName() }; + } + bc.registerService(classes, localRepository, properties); + + // TODO make it configurable + registerRepositoryServlets(dataModelName, localRepository); + if (log.isTraceEnabled()) + log.trace("Published data model " + dataModelName); + } + +// @Override +// public synchronized Long getAvailableSince() { +// return availableSince; +// } +// +// public synchronized boolean isAvailable() { +// return availableSince != null; +// } + + protected void registerRepositoryServlets(String alias, Repository repository) { + registerRemotingServlet(alias, repository); + registerWebdavServlet(alias, repository); + } + + protected void registerWebdavServlet(String alias, Repository repository) { + CmsWebDavServlet webdavServlet = new CmsWebDavServlet(alias, repository); + Hashtable ip = new Hashtable<>(); + ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsWebDavServlet.INIT_PARAM_RESOURCE_CONFIG, webDavConfig); + ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsWebDavServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, + "/" + alias); + + ip.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/" + alias + "/*"); + ip.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, + "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH + "=" + CmsConstants.PATH_DATA + ")"); + bc.registerService(Servlet.class, webdavServlet, ip); + } + + protected void registerRemotingServlet(String alias, Repository repository) { + CmsRemotingServlet remotingServlet = new CmsRemotingServlet(alias, repository); + Hashtable ip = new Hashtable<>(); + ip.put(CmsConstants.CN, alias); + // Properties ip = new Properties(); + ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_RESOURCE_PATH_PREFIX, + "/" + alias); + ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.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 RuntimeException("Cannot create temp directory for remoting servlet", e); + } + ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_HOME, tmpDir.toString()); + ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_TMP_DIRECTORY, + "remoting_" + alias); + ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_PROTECTED_HANDLERS_CONFIG, + JcrHttpUtils.DEFAULT_PROTECTED_HANDLERS); + ip.put(HTTP_WHITEBOARD_SERVLET_INIT_PARAM_PREFIX + CmsRemotingServlet.INIT_PARAM_CREATE_ABSOLUTE_URI, "false"); + + ip.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_SERVLET_PATTERN, "/" + alias + "/*"); + ip.put(HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_SELECT, + "(" + HttpWhiteboardConstants.HTTP_WHITEBOARD_CONTEXT_PATH + "=" + CmsConstants.PATH_JCR + ")"); + bc.registerService(Servlet.class, remotingServlet, ip); + } + + private class RepositoryContextStc extends ServiceTracker { + + public RepositoryContextStc() { + super(bc, RepositoryContext.class, null); + } + + @Override + public RepositoryContext addingService(ServiceReference reference) { + RepositoryContext repoContext = bc.getService(reference); + String cn = (String) reference.getProperty(CmsConstants.CN); + if (cn != null) { + List publishAsLocalRepo = new ArrayList<>(); + if (cn.equals(CmsConstants.NODE_REPOSITORY)) { +// JackrabbitDataModelMigration.clearRepositoryCaches(repoContext.getRepositoryConfig()); + prepareNodeRepository(repoContext.getRepository(), publishAsLocalRepo); + // TODO separate home repository + prepareHomeRepository(repoContext.getRepository()); + registerRepositoryServlets(cn, repoContext.getRepository()); + nodeAvailable = true; +// checkReadiness(); + } else { + prepareDataModel(cn, repoContext.getRepository(), publishAsLocalRepo); + } + // Publish all at once, so that bundles with multiple CNDs are consistent + for (String dataModelName : publishAsLocalRepo) + publishLocalRepo(dataModelName, repoContext.getRepository()); + } + return repoContext; + } + + @Override + public void modifiedService(ServiceReference reference, RepositoryContext service) { + } + + @Override + public void removedService(ServiceReference reference, RepositoryContext service) { + } + + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrFsProvider.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrFsProvider.java new file mode 100644 index 0000000..0099b3b --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsJcrFsProvider.java @@ -0,0 +1,133 @@ +package org.argeo.cms.jcr.internal; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.Path; +import java.nio.file.spi.FileSystemProvider; +import java.util.HashMap; +import java.util.Map; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.auth.CurrentUser; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.jackrabbit.fs.AbstractJackrabbitFsProvider; +import org.argeo.jcr.fs.JcrFileSystem; +import org.argeo.jcr.fs.JcrFileSystemProvider; +import org.argeo.jcr.fs.JcrFsException; + +/** Implementation of an {@link FileSystemProvider} based on Jackrabbit. */ +public class CmsJcrFsProvider extends AbstractJackrabbitFsProvider { + private Map fileSystems = new HashMap<>(); + + private RepositoryFactory repositoryFactory; + private Repository repository; + + @Override + public String getScheme() { + return CmsConstants.SCHEME_NODE; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { +// BundleContext bc = FrameworkUtil.getBundle(CmsJcrFsProvider.class).getBundleContext(); + 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 = CmsJcrUtils.getRepositoryByUri(repositoryFactory, repoUri.toString()); + CmsFileSystem fileSystem = new CmsFileSystem(this, repository); + fileSystems.put(username, fileSystem); + return fileSystem; + } else { +// Repository repository = bc.getService( +// bc.getServiceReferences(Repository.class, "(cn=" + CmsConstants.EGO_REPOSITORY + ")") +// .iterator().next()); + + // Session session = repository.login(); + CmsFileSystem fileSystem = new CmsFileSystem(this, repository); + fileSystems.put(username, fileSystem); + return fileSystem; + } + } catch (URISyntaxException e) { + throw new IllegalArgumentException("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()); + } 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(Repository repository) { + try { + Session session = repository.login(CmsConstants.HOME_WORKSPACE); + return CmsJcrUtils.getUserHome(session); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get user home", e); + } + } + + public void setRepositoryFactory(RepositoryFactory repositoryFactory) { + this.repositoryFactory = repositoryFactory; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + static class CmsFileSystem extends JcrFileSystem { + public CmsFileSystem(JcrFileSystemProvider provider, Repository repository) throws IOException { + super(provider, repository); + } + + public boolean skipNode(Node node) throws RepositoryException { +// if (node.isNodeType(NodeType.NT_HIERARCHY_NODE) || node.isNodeType(NodeTypes.NODE_USER_HOME) +// || node.isNodeType(NodeTypes.NODE_GROUP_HOME)) + if (node.isNodeType(NodeType.NT_HIERARCHY_NODE)) + return false; + // FIXME Better identifies home + if (node.hasProperty(Property.JCR_ID)) + return false; + return true; + } + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsPaths.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsPaths.java new file mode 100644 index 0000000..090ec67 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsPaths.java @@ -0,0 +1,19 @@ +package org.argeo.cms.jcr.internal; + +import java.nio.file.Path; + +/** Centralises access to the default node deployment directories. */ +@Deprecated +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); + } + + /** Singleton. */ + private CmsPaths() { + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsWorkspaceIndexer.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsWorkspaceIndexer.java new file mode 100644 index 0000000..69b98dc --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/CmsWorkspaceIndexer.java @@ -0,0 +1,342 @@ +package org.argeo.cms.jcr.internal; + +import java.util.GregorianCalendar; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.PropertyType; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; +import javax.jcr.version.VersionManager; + +import org.apache.jackrabbit.api.JackrabbitValue; +import org.apache.jackrabbit.core.RepositoryImpl; +import org.argeo.api.cms.CmsLog; +import org.argeo.jcr.JcrUtils; + +/** Ensure consistency of files, folder and last modified nodes. */ +class CmsWorkspaceIndexer implements EventListener { + private final static CmsLog log = CmsLog.getLog(CmsWorkspaceIndexer.class); + +// private final static String MIX_ETAG = "mix:etag"; + private final static String JCR_ETAG = "jcr:etag"; +// private final static String JCR_LAST_MODIFIED = "jcr:lastModified"; +// private final static String JCR_LAST_MODIFIED_BY = "jcr:lastModifiedBy"; +// private final static String JCR_MIXIN_TYPES = "jcr:mixinTypes"; + private final static String JCR_DATA = "jcr:data"; + private final static String JCR_CONTENT = "jcr:data"; + + private String cn; + private String workspaceName; + private RepositoryImpl repositoryImpl; + private Session session; + private VersionManager versionManager; + + private LinkedBlockingDeque toProcess = new LinkedBlockingDeque<>(); + private IndexingThread indexingThread; + private AtomicBoolean stopping = new AtomicBoolean(false); + + public CmsWorkspaceIndexer(RepositoryImpl repositoryImpl, String cn, String workspaceName) + throws RepositoryException { + this.cn = cn; + this.workspaceName = workspaceName; + this.repositoryImpl = repositoryImpl; + } + + public void init() { + session = KernelUtils.openAdminSession(repositoryImpl, workspaceName); + try { + String[] nodeTypes = { NodeType.NT_FILE, NodeType.MIX_LAST_MODIFIED }; + session.getWorkspace().getObservationManager().addEventListener(this, + Event.NODE_ADDED | Event.PROPERTY_CHANGED, "/", true, null, nodeTypes, true); + versionManager = session.getWorkspace().getVersionManager(); + + indexingThread = new IndexingThread(); + indexingThread.start(); + } catch (RepositoryException e1) { + throw new IllegalStateException(e1); + } + } + + public void destroy() { + stopping.set(true); + indexingThread.interrupt(); + // TODO make it configurable + try { + indexingThread.join(10 * 60 * 1000); + } catch (InterruptedException e1) { + log.warn("Indexing thread interrupted. Will log out session."); + } + + try { + session.getWorkspace().getObservationManager().removeEventListener(this); + } catch (RepositoryException e) { + if (log.isTraceEnabled()) + log.warn("Cannot unregistered JCR event listener", e); + } finally { + JcrUtils.logoutQuietly(session); + } + } + + private synchronized void processEvents(EventIterator events) { + long begin = System.currentTimeMillis(); + long count = 0; + while (events.hasNext()) { + Event event = events.nextEvent(); + try { + toProcess.put(event); + } catch (InterruptedException e) { + e.printStackTrace(); + } +// processEvent(event); + count++; + } + long duration = System.currentTimeMillis() - begin; + if (log.isTraceEnabled()) + log.trace("Processed " + count + " events in " + duration + " ms"); + notifyAll(); + } + + protected synchronized void processEvent(Event event) { + try { + String eventPath = event.getPath(); + if (event.getType() == Event.NODE_ADDED) { + if (!versionManager.isCheckedOut(eventPath)) + return;// ignore checked-in nodes + if (log.isTraceEnabled()) + log.trace("NODE_ADDED " + eventPath); +// session.refresh(true); + session.refresh(false); + Node node = session.getNode(eventPath); + Node parentNode = node.getParent(); + if (parentNode.isNodeType(NodeType.NT_FILE)) { + if (node.isNodeType(NodeType.NT_UNSTRUCTURED)) { + if (!node.isNodeType(NodeType.MIX_LAST_MODIFIED)) + node.addMixin(NodeType.MIX_LAST_MODIFIED); + Property property = node.getProperty(Property.JCR_DATA); + String etag = toEtag(property.getValue()); + session.save(); + node.setProperty(JCR_ETAG, etag); + if (log.isTraceEnabled()) + log.trace("ETag and last modified added to new " + node); + } else if (node.isNodeType(NodeType.NT_RESOURCE)) { +// if (!node.isNodeType(MIX_ETAG)) +// node.addMixin(MIX_ETAG); +// session.save(); +// Property property = node.getProperty(Property.JCR_DATA); +// String etag = toEtag(property.getValue()); +// node.setProperty(JCR_ETAG, etag); +// session.save(); + } +// setLastModifiedRecursive(parentNode, event); +// session.save(); +// if (log.isTraceEnabled()) +// log.trace("ETag and last modified added to new " + node); + } + +// if (node.isNodeType(NodeType.NT_FOLDER)) { +// setLastModifiedRecursive(node, event); +// session.save(); +// if (log.isTraceEnabled()) +// log.trace("Last modified added to new " + node); +// } + } else if (event.getType() == Event.PROPERTY_CHANGED) { + String propertyName = extractItemName(eventPath); + // skip if last modified properties are explicitly set + if (!propertyName.equals(JCR_DATA)) + return; +// if (propertyName.equals(JCR_LAST_MODIFIED)) +// return; +// if (propertyName.equals(JCR_LAST_MODIFIED_BY)) +// return; +// if (propertyName.equals(JCR_MIXIN_TYPES)) +// return; +// if (propertyName.equals(JCR_ETAG)) +// return; + + if (log.isTraceEnabled()) + log.trace("PROPERTY_CHANGED " + eventPath); + + if (!session.propertyExists(eventPath)) + return; + session.refresh(false); + Property property = session.getProperty(eventPath); + Node node = property.getParent(); + if (property.getType() == PropertyType.BINARY && propertyName.equals(JCR_DATA) + && node.isNodeType(NodeType.NT_UNSTRUCTURED)) { + String etag = toEtag(property.getValue()); + node.setProperty(JCR_ETAG, etag); + Node parentNode = node.getParent(); + if (parentNode.isNodeType(NodeType.MIX_LAST_MODIFIED)) { + setLastModified(parentNode, event); + } + if (log.isTraceEnabled()) + log.trace("ETag and last modified updated for " + node); + } +// setLastModified(node, event); +// session.save(); +// if (log.isTraceEnabled()) +// log.trace("ETag and last modified updated for " + node); + } else if (event.getType() == Event.NODE_REMOVED) { + String removeNodePath = eventPath; + String nodeName = extractItemName(eventPath); + if (JCR_CONTENT.equals(nodeName)) // parent is a file, deleted anyhow + return; + if (log.isTraceEnabled()) + log.trace("NODE_REMOVED " + eventPath); +// String parentPath = JcrUtils.parentPath(removeNodePath); +// session.refresh(true); +// setLastModified(parentPath, event); +// session.save(); + if (log.isTraceEnabled()) + log.trace("Last modified updated for parents of removed " + removeNodePath); + } + } catch (Exception e) { + if (log.isTraceEnabled()) + log.warn("Cannot process event " + event, e); + } finally { +// try { +// session.refresh(true); +// if (session.hasPendingChanges()) +// session.save(); +//// session.refresh(false); +// } catch (RepositoryException e) { +// if (log.isTraceEnabled()) +// log.warn("Cannot refresh JCR session", e); +// } + } + + } + + private String extractItemName(String path) { + if (path == null || path.length() <= 1) + return null; + int lastIndex = path.lastIndexOf('/'); + if (lastIndex >= 0) { + return path.substring(lastIndex + 1); + } else { + return path; + } + } + + @Override + public void onEvent(EventIterator events) { + processEvents(events); +// Runnable toRun = new Runnable() { +// +// @Override +// public void run() { +// processEvents(events); +// } +// }; +// Future future = Activator.getInternalExecutorService().submit(toRun); +// try { +// // make the call synchronous +// future.get(60, TimeUnit.SECONDS); +// } catch (TimeoutException | ExecutionException | InterruptedException e) { +// // silent +// } + } + + static String toEtag(Value v) { + if (v instanceof JackrabbitValue) { + JackrabbitValue value = (JackrabbitValue) v; + return '\"' + value.getContentIdentity() + '\"'; + } else { + return null; + } + + } + + protected synchronized void setLastModified(Node node, Event event) throws RepositoryException { + GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTimeInMillis(event.getDate()); + node.setProperty(Property.JCR_LAST_MODIFIED, calendar); + node.setProperty(Property.JCR_LAST_MODIFIED_BY, event.getUserID()); + if (log.isTraceEnabled()) + log.trace("Last modified set on " + node); + } + + /** Recursively set the last updated time on parents. */ + protected synchronized void setLastModifiedRecursive(Node node, Event event) throws RepositoryException { + if (versionManager.isCheckedOut(node.getPath())) { + if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) { + setLastModified(node, event); + } + if (node.isNodeType(NodeType.NT_FOLDER) && !node.isNodeType(NodeType.MIX_LAST_MODIFIED)) { + node.addMixin(NodeType.MIX_LAST_MODIFIED); + if (log.isTraceEnabled()) + log.trace("Last modified mix-in added to " + node); + } + + } + + // end condition + if (node.getDepth() == 0) { +// try { +// node.getSession().save(); +// } catch (RepositoryException e) { +// log.warn("Cannot index workspace", e); +// } + return; + } else { + Node parent = node.getParent(); + setLastModifiedRecursive(parent, event); + } + } + + /** + * Recursively set the last updated time on parents. Useful to use paths when + * dealing with deletions. + */ + protected synchronized void setLastModifiedRecursive(String path, Event event) throws RepositoryException { + // root node will always exist, so end condition is delegated to the other + // recursive setLastModified method + if (session.nodeExists(path)) { + setLastModifiedRecursive(session.getNode(path), event); + } else { + setLastModifiedRecursive(JcrUtils.parentPath(path), event); + } + } + + @Override + public String toString() { + return "Indexer for workspace " + workspaceName + " of repository " + cn; + } + + class IndexingThread extends Thread { + + public IndexingThread() { + super(CmsWorkspaceIndexer.this.toString()); + // TODO Auto-generated constructor stub + } + + @Override + public void run() { + life: while (session != null && session.isLive()) { + try { + Event nextEvent = toProcess.take(); + processEvent(nextEvent); + } catch (InterruptedException e) { + // silent + interrupted(); + } + + if (stopping.get() && toProcess.isEmpty()) { + break life; + } + } + if (log.isDebugEnabled()) + log.debug(CmsWorkspaceIndexer.this.toString() + " has shut down."); + } + + } + +} \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/DataModels.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/DataModels.java new file mode 100644 index 0000000..f2196bd --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/DataModels.java @@ -0,0 +1,190 @@ +package org.argeo.cms.jcr.internal; + +import static org.argeo.cms.osgi.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.argeo.api.cms.CmsLog; +import org.argeo.cms.osgi.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 CmsLog log = CmsLog.getLog(DataModels.class); + + private Map dataModels = new TreeMap<>(); + + public DataModels(BundleContext bc) { + for (Bundle bundle : bc.getBundles()) + processBundle(bundle, null); + bc.addBundleListener(this); + } + + public List getNonAbstractDataModels() { + List 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(), null); + } else if (event.getType() == Bundle.UNINSTALLED) { + BundleWiring wiring = event.getBundle().adapt(BundleWiring.class); + List 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, List scannedBundles) { + if (scannedBundles != null && scannedBundles.contains(bundle)) + throw new IllegalStateException("Cycle in CMS data model requirements for " + bundle); + BundleWiring wiring = bundle.adapt(BundleWiring.class); + if (wiring == null) { + int bundleState = bundle.getState(); + if (bundleState != Bundle.INSTALLED && bundleState != Bundle.UNINSTALLED) {// ignore unresolved bundles + log.warn("Bundle " + bundle.getSymbolicName() + " #" + bundle.getBundleId() + " (" + + bundle.getLocation() + ") cannot be adapted to a wiring"); + } else { + if (log.isTraceEnabled()) + log.warn("Bundle " + bundle.getSymbolicName() + " is not resolved."); + } + return; + } + List providedDataModels = wiring.getCapabilities(CMS_DATA_MODEL_NAMESPACE); + if (providedDataModels.size() == 0) + return; + List requiredDataModels = wiring.getRequiredWires(CMS_DATA_MODEL_NAMESPACE); + // process requirements first + for (BundleWire bundleWire : requiredDataModels) { + List nextScannedBundles = new ArrayList<>(); + if (scannedBundles != null) + nextScannedBundles.addAll(scannedBundles); + nextScannedBundles.add(bundle); + Bundle providerBundle = bundleWire.getProvider().getBundle(); + processBundle(providerBundle, nextScannedBundles); + } + 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 required; + + private DataModel(String name, BundleCapability bundleCapability, List requiredDataModels) { + assert CMS_DATA_MODEL_NAMESPACE.equals(bundleCapability.getNamespace()); + this.name = name; + Map 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 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 IllegalStateException("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 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.jcr/src/org/argeo/cms/jcr/internal/EgoRepository.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/EgoRepository.java new file mode 100644 index 0000000..ef785f9 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/EgoRepository.java @@ -0,0 +1,264 @@ +package org.argeo.cms.jcr.internal; + +import java.security.PrivilegedAction; +import java.text.SimpleDateFormat; +import java.util.HashSet; +import java.util.Set; + +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 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.api.cms.CmsAuth; +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrRepositoryWrapper; +import org.argeo.jcr.JcrUtils; + +/** + * Make sure each user has a home directory available. + */ +class EgoRepository extends JcrRepositoryWrapper { + + /** 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 checkedUsers = new HashSet(); + + private SimpleDateFormat usersDatePath = new SimpleDateFormat("YYYY/MM"); + + private String defaultHomeWorkspace = CmsConstants.HOME_WORKSPACE; + private String defaultGroupsWorkspace = CmsConstants.SRV_WORKSPACE; +// private String defaultGuestsWorkspace = NodeConstants.GUESTS_WORKSPACE; + private final boolean remote; + + public EgoRepository(Repository repository, boolean remote) { + super(repository); + this.remote = remote; + putDescriptor(CmsConstants.CN, CmsConstants.EGO_REPOSITORY); + if (!remote) { + LoginContext lc; + try { + lc = new LoginContext(CmsAuth.LOGIN_CONTEXT_DATA_ADMIN); + lc.login(); + } catch (javax.security.auth.login.LoginException e1) { + throw new IllegalStateException("Cannot login as system", e1); + } + Subject.doAs(lc.getSubject(), new PrivilegedAction() { + + @Override + public Void run() { + loginOrCreateWorkspace(defaultHomeWorkspace); + loginOrCreateWorkspace(defaultGroupsWorkspace); + return null; + } + + }); + } + } + + private void loginOrCreateWorkspace(String workspace) { + Session adminSession = null; + try { + adminSession = JcrUtils.loginOrCreateWorkspace(getRepository(workspace), workspace); +// JcrUtils.addPrivilege(adminSession, "/", NodeConstants.ROLE_USER, Privilege.JCR_READ); + +// initJcr(adminSession); + } catch (RepositoryException e) { + throw new JcrException("Cannot init JCR home", e); + } finally { + JcrUtils.logoutQuietly(adminSession); + } + } + +// @Override +// public Session login(Credentials credentials, String workspaceName) +// throws LoginException, NoSuchWorkspaceException, RepositoryException { +// if (workspaceName == null) { +// return super.login(credentials, getUserHomeWorkspace()); +// } else { +// return super.login(credentials, workspaceName); +// } +// } + + protected String getUserHomeWorkspace() { + // TODO base on JAAS Subject metadata + return defaultHomeWorkspace; + } + + protected String getGroupsWorkspace() { + // TODO base on JAAS Subject metadata + return defaultGroupsWorkspace; + } + +// protected String getGuestsWorkspace() { +// // TODO base on JAAS Subject metadata +// return defaultGuestsWorkspace; +// } + + @Override + protected void processNewSession(Session session, String workspaceName) { + String username = session.getUserID(); + if (username == null || username.toString().equals("")) + return; + if (session.getUserID().equals(CmsConstants.ROLE_ANONYMOUS)) + return; + + String userHomeWorkspace = getUserHomeWorkspace(); + if (workspaceName == null || !workspaceName.equals(userHomeWorkspace)) + return; + + if (checkedUsers.contains(username)) + return; + Session adminSession = KernelUtils.openAdminSession(getRepository(workspaceName), workspaceName); + 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 JcrException("Cannot initialize home repository", e); + } finally { + JcrUtils.logoutQuietly(adminSession); + } + } + + protected synchronized void syncJcr(Session adminSession, String username) { + // only in the default workspace +// if (workspaceName != null) +// return; + // skip system users + if (username.endsWith(CmsConstants.SYSTEM_ROLES_BASEDN)) + return; + + try { + Node userHome = CmsJcrUtils.getUserHome(adminSession, username); + if (userHome == null) { +// String homePath = generateUserPath(username); + String userId = extractUserId(username); +// if (adminSession.itemExists(homePath))// duplicate user id +// userHome = adminSession.getNode(homePath).getParent().addNode(JcrUtils.lastPathElement(homePath)); +// else +// userHome = JcrUtils.mkdirs(adminSession, homePath); + userHome = adminSession.getRootNode().addNode(userId); +// userHome.addMixin(NodeTypes.NODE_USER_HOME); + userHome.addMixin(NodeType.MIX_CREATED); + userHome.addMixin(NodeType.MIX_TITLE); + userHome.setProperty(Property.JCR_ID, username); + // TODO use display name + userHome.setProperty(Property.JCR_TITLE, userId); +// userHome.setProperty(NodeNames.LDAP_UID, username); + adminSession.save(); + + JcrUtils.clearAccessControList(adminSession, userHome.getPath(), username); + JcrUtils.addPrivilege(adminSession, userHome.getPath(), username, Privilege.JCR_ALL); +// JackrabbitSecurityUtils.denyPrivilege(adminSession, userHome.getPath(), NodeConstants.ROLE_USER, +// Privilege.JCR_READ); + } + if (adminSession.hasPendingChanges()) + adminSession.save(); + } catch (RepositoryException e) { + JcrUtils.discardQuietly(adminSession); + throw new JcrException("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 IllegalArgumentException("Invalid name " + username, e); + } + String userId = dn.getRdn(dn.size() - 1).getValue().toString(); + return '/' + userId; +// int atIndex = userId.indexOf('@'); +// if (atIndex < 0) { +// return homeBasePath+'/' + userId; +// } else { +// return usersBasePath + '/' + usersDatePath.format(new Date()) + '/' + userId; +// } + } + + private String extractUserId(String username) { + LdapName dn; + try { + dn = new LdapName(username); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Invalid name " + username, e); + } + String userId = dn.getRdn(dn.size() - 1).getValue().toString(); + return userId; +// int atIndex = userId.indexOf('@'); +// if (atIndex < 0) { +// return homeBasePath+'/' + userId; +// } else { +// return usersBasePath + '/' + usersDatePath.format(new Date()) + '/' + userId; +// } + } + + public void createWorkgroup(LdapName dn) { + String groupsWorkspace = getGroupsWorkspace(); + Session adminSession = KernelUtils.openAdminSession(getRepository(groupsWorkspace), groupsWorkspace); + String cn = dn.getRdn(dn.size() - 1).getValue().toString(); + Node newWorkgroup = CmsJcrUtils.getGroupHome(adminSession, cn); + if (newWorkgroup != null) { + JcrUtils.logoutQuietly(adminSession); + throw new IllegalStateException("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 = adminSession.getRootNode().addNode(relPath, NodeType.NT_UNSTRUCTURED); +// newWorkgroup = JcrUtils.mkdirs(adminSession.getNode(groupsBasePath), relPath, NodeType.NT_UNSTRUCTURED); +// newWorkgroup.addMixin(NodeTypes.NODE_GROUP_HOME); + newWorkgroup.addMixin(NodeType.MIX_CREATED); + newWorkgroup.addMixin(NodeType.MIX_TITLE); + newWorkgroup.setProperty(Property.JCR_ID, dn.toString()); + newWorkgroup.setProperty(Property.JCR_TITLE, cn); +// 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 JcrException("Cannot create workgroup", e); + } finally { + JcrUtils.logoutQuietly(adminSession); + } + + } + + public boolean isRemote() { + return remote; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JackrabbitLocalRepository.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JackrabbitLocalRepository.java new file mode 100644 index 0000000..bad9fdf --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JackrabbitLocalRepository.java @@ -0,0 +1,71 @@ +package org.argeo.cms.jcr.internal; + +import java.util.Map; +import java.util.TreeMap; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.jackrabbit.core.RepositoryImpl; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; + +class JackrabbitLocalRepository extends LocalRepository { + private final static CmsLog log = CmsLog.getLog(JackrabbitLocalRepository.class); + final String SECURITY_WORKSPACE = "security"; + + private Map workspaceMonitors = new TreeMap<>(); + + public JackrabbitLocalRepository(RepositoryImpl repository, String cn) { + super(repository, cn); +// Session session = KernelUtils.openAdminSession(repository); +// try { +// if (NodeConstants.NODE.equals(cn)) +// for (String workspaceName : session.getWorkspace().getAccessibleWorkspaceNames()) { +// addMonitor(workspaceName); +// } +// } catch (RepositoryException e) { +// throw new IllegalStateException(e); +// } finally { +// JcrUtils.logoutQuietly(session); +// } + } + + protected RepositoryImpl getJackrabbitrepository(String workspaceName) { + return (RepositoryImpl) getRepository(workspaceName); + } + + @Override + protected synchronized void processNewSession(Session session, String workspaceName) { +// String realWorkspaceName = session.getWorkspace().getName(); +// addMonitor(realWorkspaceName); + } + + private void addMonitor(String realWorkspaceName) { + if (realWorkspaceName.equals(SECURITY_WORKSPACE)) + return; + if (!CmsConstants.NODE_REPOSITORY.equals(getCn())) + return; + + if (!workspaceMonitors.containsKey(realWorkspaceName)) { + try { + CmsWorkspaceIndexer workspaceMonitor = new CmsWorkspaceIndexer( + getJackrabbitrepository(realWorkspaceName), getCn(), realWorkspaceName); + workspaceMonitors.put(realWorkspaceName, workspaceMonitor); + workspaceMonitor.init(); + if (log.isDebugEnabled()) + log.debug("Registered " + workspaceMonitor); + } catch (RepositoryException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + } + + public void destroy() { + for (String workspaceName : workspaceMonitors.keySet()) { + workspaceMonitors.get(workspaceName).destroy(); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JcrKeyring.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JcrKeyring.java new file mode 100644 index 0000000..17625f5 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JcrKeyring.java @@ -0,0 +1,397 @@ +package org.argeo.cms.jcr.internal; + +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.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.NoSuchPaddingException; +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.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.ArgeoNames; +import org.argeo.cms.ArgeoTypes; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.cms.security.AbstractKeyring; +import org.argeo.cms.security.PBEKeySpecCallback; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; + +/** JCR based implementation of a keyring */ +public class JcrKeyring extends AbstractKeyring implements ArgeoNames { + private final static CmsLog log = CmsLog.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 sessionThreadLocal = new ThreadLocal() { + + @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 notYetSavedKeyring = new ThreadLocal() { + // + // @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(CmsConstants.HOME_WORKSPACE); + } catch (RepositoryException e) { + throw new JcrException("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 = CmsJcrUtils.getUserHome(session); + return userHome.hasNode(ARGEO_KEYRING); + } catch (RepositoryException e) { + throw new JcrException("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 = CmsJcrUtils.getUserHome(session()); + Node keyring; + if (userHome.hasNode(ARGEO_KEYRING)) { + throw new IllegalArgumentException("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); + } catch (IOException e) { + throw new RuntimeException("Cannot set keyring salt", e); + } + + 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 (RepositoryException e) { + throw new JcrException("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 = CmsJcrUtils.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 IllegalStateException("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 JcrException("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 IllegalStateException("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 JcrException("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 (RepositoryException e) { + throw new JcrException("Cannot encrypt", e); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Cannot encrypt", e); + } + } + + @Override + protected synchronized InputStream decrypt(String path) { + Binary binary = 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 (RepositoryException e) { + throw new JcrException("Cannot decrypt", e); + } catch (GeneralSecurityException | IOException e) { + throw new RuntimeException("Cannot decrypt", e); + } finally { + 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 = CmsJcrUtils.getUserHome(session()); + if (!userHome.hasNode(ARGEO_KEYRING)) + throw new IllegalArgumentException("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 (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalArgumentException("Cannot get cipher", e); + } catch (RepositoryException e) { + throw new JcrException("Cannot get cipher", e); + } finally { + + } + } + + 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 (GeneralSecurityException e) { + throw new RuntimeException("Cannot change JCR keyring password", e); + } catch (RepositoryException e) { + throw new JcrException("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.jcr/src/org/argeo/cms/jcr/internal/JcrRepositoryFactory.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JcrRepositoryFactory.java new file mode 100644 index 0000000..342c1ad --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/JcrRepositoryFactory.java @@ -0,0 +1,191 @@ +package org.argeo.cms.jcr.internal; + +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.jackrabbit.jcr2dav.Jcr2davRepositoryFactory; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.internal.jcr.RepoConf; +import org.argeo.cms.jcr.internal.osgi.CmsJcrActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.ServiceReference; + +/** + * OSGi-aware Jackrabbit repository factory which can retrieve/publish + * {@link Repository} as OSGi services. + */ +public class JcrRepositoryFactory implements RepositoryFactory { + private final CmsLog log = CmsLog.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) { + BundleContext bundleContext = CmsJcrActivator.getBundleContext(); + if (bundleContext != null) { + try { + Collection> srs = bundleContext.getServiceReferences(Repository.class, + "(" + CmsConstants.CN + "=" + alias + ")"); + if (srs.size() == 0) + throw new IllegalArgumentException("No repository with alias " + alias + " found in OSGi registry"); + else if (srs.size() > 1) + throw new IllegalArgumentException( + srs.size() + " repositories with alias " + alias + " found in OSGi registry"); + return bundleContext.getService(srs.iterator().next()); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Cannot find repository with alias " + alias, e); + } + } else { + // TODO ability to filter static services + return null; + } + } + + // private void publish(String alias, Repository repository, Properties + // properties) { + // if (bundleContext != null) { + // // do not modify reference + // Hashtable props = new Hashtable(); + // 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(CmsConstants.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 IllegalArgumentException("Unrecognized URI format " + uri); + + } + + else if (parameters.containsKey(CmsConstants.CN)) { + // Properties properties = new Properties(); + // properties.putAll(parameters); + String alias = parameters.get(CmsConstants.CN).toString(); + // publish(alias, repository, properties); + // log.info("Registered JCR repository under alias '" + alias + "' + // with properties " + properties); + repository = getRepositoryByAlias(alias); + } else + throw new IllegalArgumentException("Not enough information in " + parameters); + + if (repository == null) + throw new IllegalArgumentException("Repository not found " + parameters); + + return repository; + } + + protected Repository createRemoteRepository(String uri, String defaultWorkspace) throws RepositoryException { + Map params = new HashMap(); + 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 IllegalArgumentException("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 IllegalArgumentException("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) { + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/KernelConstants.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/KernelConstants.java new file mode 100644 index 0000000..93f29fb --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/KernelConstants.java @@ -0,0 +1,53 @@ +package org.argeo.cms.jcr.internal; + +import org.argeo.api.cms.CmsConstants; + +/** Internal CMS constants. */ +@Deprecated +public interface KernelConstants { + // Directories + String DIR_NODE = "node"; + String DIR_REPOS = "repos"; + String DIR_INDEXES = "indexes"; + String DIR_TRANSACTIONS = "transactions"; + + // Files + String DEPLOY_CONFIG_PATH = DIR_NODE + '/' + CmsConstants.DEPLOY_BASEDN + ".ldif"; + String DEFAULT_KEYSTORE_PATH = DIR_NODE + '/' + CmsConstants.NODE + ".p12"; + String DEFAULT_PEM_KEY_PATH = DIR_NODE + '/' + CmsConstants.NODE + ".key"; + String DEFAULT_PEM_CERT_PATH = DIR_NODE + '/' + CmsConstants.NODE + ".crt"; + 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"; + + // KERBEROS + String DEFAULT_KERBEROS_SERVICE = "HTTP"; + + // 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"; + // default Jetty server configured via JettyConfigurator + String DEFAULT_JETTY_SERVER = "default"; + String CMS_JETTY_CUSTOMIZER_CLASS = "org.argeo.equinox.jetty.CmsJettyCustomizer"; + + // 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.jcr/src/org/argeo/cms/jcr/internal/KernelUtils.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/KernelUtils.java new file mode 100644 index 0000000..edfe87a --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/KernelUtils.java @@ -0,0 +1,262 @@ +package org.argeo.cms.jcr.internal; + +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.argeo.api.cms.CmsAuth; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.jcr.internal.osgi.CmsJcrActivator; +import org.argeo.cms.osgi.DataModelNamespace; +import org.osgi.framework.BundleContext; +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 IllegalArgumentException("Cannot set configuration " + jaasConfigurationUrl, e); + } + } + + static Dictionary asDictionary(Properties props) { + Hashtable hashtable = new Hashtable(); + for (Object key : props.keySet()) { + hashtable.put(key.toString(), props.get(key)); + } + return hashtable; + } + + static Dictionary asDictionary(ClassLoader cl, String resource) { + Properties props = new Properties(); + try { + props.load(cl.getResourceAsStream(resource)); + } catch (IOException e) { + throw new IllegalArgumentException("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 IllegalArgumentException("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); + if (osgiInstanceBaseUri != null) + return safeUri(osgiInstanceBaseUri + (relativePath != null ? relativePath : "")); + else + return Paths.get(System.getProperty("user.dir")).toUri(); + } + + static File getOsgiConfigurationFile(String relativePath) { + try { + return new File(new URI(getBundleContext().getProperty(OSGI_CONFIGURATION_AREA) + relativePath)) + .getCanonicalFile(); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot get configuration file for " + relativePath, e); + } + } + + static String getFrameworkProp(String key, String def) { + BundleContext bundleContext = CmsJcrActivator.getBundleContext(); + String value; + if (bundleContext != null) + value = bundleContext.getProperty(key); + else + value = System.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(CmsLog log) { + BundleContext bc = getBundleContext(); + for (Object sysProp : new TreeSet(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 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) { + LoginContext loginContext = loginAsDataAdmin(); + return Subject.doAs(loginContext.getSubject(), new PrivilegedAction() { + + @Override + public Session run() { + try { + return repository.login(workspaceName); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot open admin session", e); + } finally { + try { + loginContext.logout(); + } catch (LoginException e) { + throw new IllegalStateException(e); + } + } + } + + }); + } + + static LoginContext loginAsDataAdmin() { + ClassLoader currentCl = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(KernelUtils.class.getClassLoader()); + LoginContext loginContext; + try { + loginContext = new LoginContext(CmsAuth.LOGIN_CONTEXT_DATA_ADMIN); + loginContext.login(); + } catch (LoginException e1) { + throw new IllegalStateException("Could not login as data admin", e1); + } finally { + Thread.currentThread().setContextClassLoader(currentCl); + } + return loginContext; + } + + static void doAsDataAdmin(Runnable action) { + LoginContext loginContext = loginAsDataAdmin(); + Subject.doAs(loginContext.getSubject(), new PrivilegedAction() { + + @Override + public Void run() { + try { + action.run(); + return null; + } finally { + try { + loginContext.logout(); + } catch (LoginException e) { + throw new IllegalStateException(e); + } + } + } + + }); + } + + static void asyncOpen(ServiceTracker st) { + Runnable run = new Runnable() { + + @Override + public void run() { + st.open(); + } + }; +// Activator.getInternalExecutorService().execute(run); + new Thread(run, "Open service tracker " + st).start(); + } + + static BundleContext getBundleContext() { + return CmsJcrActivator.getBundleContext(); + } + + static boolean asBoolean(String value) { + if (value == null) + return false; + switch (value) { + case "true": + return true; + case "false": + return false; + default: + throw new IllegalArgumentException( + "Unsupported value for attribute " + DataModelNamespace.ABSTRACT + ": " + value); + } + } + + private static URI safeUri(String uri) { + if (uri == null) + throw new IllegalArgumentException("URI cannot be null"); + try { + return new URI(uri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Badly formatted URI " + uri, e); + } + } + + private KernelUtils() { + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/LocalRepository.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/LocalRepository.java new file mode 100644 index 0000000..0bac94c --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/LocalRepository.java @@ -0,0 +1,23 @@ +package org.argeo.cms.jcr.internal; + +import javax.jcr.Repository; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.jcr.JcrRepositoryWrapper; + +class LocalRepository extends JcrRepositoryWrapper { + private final String cn; + + public LocalRepository(Repository repository, String cn) { + super(repository); + this.cn = cn; + // Map attrs = dataModelCapability.getAttributes(); + // cn = (String) attrs.get(DataModelNamespace.NAME); + putDescriptor(CmsConstants.CN, cn); + } + + String getCn() { + return cn; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/NodeKeyRing.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/NodeKeyRing.java new file mode 100644 index 0000000..9cd1f72 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/NodeKeyRing.java @@ -0,0 +1,19 @@ +package org.argeo.cms.jcr.internal; + +import java.util.Dictionary; + +import javax.jcr.Repository; + +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 properties) throws ConfigurationException { + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/RepositoryContextsFactory.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/RepositoryContextsFactory.java new file mode 100644 index 0000000..f3a099b --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/RepositoryContextsFactory.java @@ -0,0 +1,191 @@ +package org.argeo.cms.jcr.internal; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; + +import org.apache.jackrabbit.core.RepositoryContext; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.CmsState; +import org.argeo.cms.CmsDeployProperty; +import org.argeo.cms.internal.jcr.RepoConf; +import org.argeo.cms.internal.jcr.RepositoryBuilder; +import org.argeo.cms.jcr.internal.osgi.CmsJcrActivator; +import org.argeo.util.LangUtils; +import org.osgi.service.cm.ManagedServiceFactory; + +/** A {@link ManagedServiceFactory} creating or referencing JCR repositories. */ +public class RepositoryContextsFactory { + private final static CmsLog log = CmsLog.getLog(RepositoryContextsFactory.class); + private final static String NODE_REPO_PROP_PREFIX = "argeo.node.repo."; +// private final BundleContext bc = FrameworkUtil.getBundle(RepositoryServiceFactory.class).getBundleContext(); + +// private Map repositories = new HashMap(); +// private Map pidToCn = new HashMap(); + + private RepositoryContext repositoryContext; + + private CmsState cmsState; + + public void init() { + Dictionary config = getNodeRepositoryConfig(); + deployRepository(config); + } + + public void destroy() { + if (this.repositoryContext != null) { + this.repositoryContext.getRepository().shutdown(); + } +// for (String pid : repositories.keySet()) { +// try { +// RepositoryContext repositoryContext = repositories.get(pid); +// // Must start in another thread otherwise shutdown is interrupted +// // TODO use an executor? +// new Thread(() -> { +// repositoryContext.getRepository().shutdown(); +// if (log.isDebugEnabled()) +// log.debug("Shut down repository " + pid +// + (pidToCn.containsKey(pid) ? " (" + pidToCn.get(pid) + ")" : "")); +// }, "Shutdown JCR repository " + pid).start(); +// } catch (Exception e) { +// log.error("Error when shutting down Jackrabbit repository " + pid, e); +// } +// } + } + +// @Override +// public String getName() { +// return "Jackrabbit repository service factory"; +// } + + /** Override the provided config with the framework properties */ + private Dictionary getNodeRepositoryConfig() { + Dictionary props = new Hashtable(); + addDeployProperty(CmsDeployProperty.DB_URL, RepoConf.dburl, props); + addDeployProperty(CmsDeployProperty.DB_USER, RepoConf.dbuser, props); + addDeployProperty(CmsDeployProperty.DB_PASSWORD, RepoConf.dbpassword, props); + for (RepoConf repoConf : RepoConf.values()) { + Object value = getFrameworkProp(NODE_REPO_PROP_PREFIX + repoConf.name()); + if (value != null) { + props.put(repoConf.name(), value); + if (log.isDebugEnabled()) + log.debug("Set node repo configuration " + repoConf.name() + " to " + value); + } + } + props.put(CmsConstants.CN, CmsConstants.NODE_REPOSITORY); + return props; + } + + private void addDeployProperty(CmsDeployProperty deployProperty, RepoConf repoConf, + Dictionary props) { + String value = getFrameworkProp(deployProperty.getProperty()); + if (value != null) { + props.put(repoConf.name(), value); + } + + } + +// @Override +// public void updated(String pid, Dictionary properties) throws ConfigurationException { + protected void deployRepository(Dictionary properties) { +// if (repositories.containsKey(pid)) +// throw new IllegalArgumentException("Already a repository registered for " + pid); + + if (properties == null) + return; + + Object cn = properties.get(CmsConstants.CN); +// if (cn != null) +// for (String otherPid : pidToCn.keySet()) { +// Object o = pidToCn.get(otherPid); +// if (cn.equals(o)) { +// RepositoryContext repositoryContext = repositories.remove(otherPid); +// repositories.put(pid, repositoryContext); +// if (log.isDebugEnabled()) +// log.debug("Ignoring update of Jackrabbit repository " + cn); +// // FIXME perform a proper update (also of the OSGi service) +// 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 props = LangUtils.dict(Constants.SERVICE_PID, pid); + Dictionary props = new Hashtable<>(); + // props.put(ArgeoJcrConstants.JCR_REPOSITORY_URI, + // properties.get(RepoConf.labeledUri.name())); + if (cn != null) { + props.put(CmsConstants.CN, cn); + // props.put(NodeConstants.JCR_REPOSITORY_ALIAS, cn); +// pidToCn.put(pid, cn); + } + CmsJcrActivator.registerService(RepositoryContext.class, repositoryContext, props); + this.repositoryContext = repositoryContext; + } else { + 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)); + RepositoryFactory repositoryFactory = CmsJcrActivator.getService(RepositoryFactory.class); + Map parameters = new HashMap(); + 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 props = LangUtils.dict(Constants.SERVICE_PID, pid); + Dictionary props = new Hashtable<>(); + props.put(RepoConf.labeledUri.name(), + new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), uri.getPath(), null, null) + .toString()); + if (cn != null) { + props.put(CmsConstants.CN, cn); +// pidToCn.put(pid, cn); + } + CmsJcrActivator.registerService(Repository.class, repository, props); + + // home + if (cn.equals(CmsConstants.NODE_REPOSITORY)) { + Dictionary homeProps = LangUtils.dict(CmsConstants.CN, CmsConstants.EGO_REPOSITORY); + EgoRepository homeRepository = new EgoRepository(repository, true); + CmsJcrActivator.registerService(Repository.class, homeRepository, homeProps); + } + } + } catch (RepositoryException | URISyntaxException | IOException e) { + throw new IllegalStateException("Cannot create Jackrabbit repository " + properties, e); + } + + } + +// @Override +// public void deleted(String pid) { +// RepositoryContext repositoryContext = repositories.remove(pid); +// repositoryContext.getRepository().shutdown(); +// if (log.isDebugEnabled()) +// log.debug("Deleted repository " + pid); +// } + + private String getFrameworkProp(String key) { + return cmsState.getDeployProperty(key); + } + + public void setCmsState(CmsState cmsState) { + this.cmsState = cmsState; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/StatisticsThread.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/StatisticsThread.java new file mode 100644 index 0000000..5a2cd5b --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/StatisticsThread.java @@ -0,0 +1,123 @@ +package org.argeo.cms.jcr.internal; + +import java.io.File; +import java.lang.management.ManagementFactory; + +import org.apache.jackrabbit.api.stats.RepositoryStatistics; +import org.apache.jackrabbit.stats.RepositoryStatisticsImpl; +import org.argeo.api.cms.CmsLog; + +/** + * Background thread started by the kernel, which gather statistics and + * monitor/control other processes. + */ +public class StatisticsThread extends Thread { + private final static CmsLog log = CmsLog.getLog(StatisticsThread.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 CmsLog kernelStatsLog = CmsLog.getLog("argeo.stats.kernel"); + private CmsLog nodeStatsLog = CmsLog.getLog("argeo.stats.node"); + + @SuppressWarnings("unused") + private long cycle = 0l; + + public StatisticsThread(String name) { + super(name); + } + + private void doSmallestPeriod() { + // Clean expired sessions + // FIXME re-enable it in CMS + //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() { + if (log.isTraceEnabled()) + log.trace("Kernel thread started."); + 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++; + } + } + + public 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.jcr/src/org/argeo/cms/jcr/internal/osgi/CmsJcrActivator.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/osgi/CmsJcrActivator.java new file mode 100644 index 0000000..57860d8 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/osgi/CmsJcrActivator.java @@ -0,0 +1,91 @@ +package org.argeo.cms.jcr.internal.osgi; + +import java.util.Dictionary; + +import org.argeo.cms.jcr.internal.StatisticsThread; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +public class CmsJcrActivator implements BundleActivator { + private static BundleContext bundleContext; + +// private List stopHooks = new ArrayList<>(); + private StatisticsThread kernelThread; + +// private JackrabbitRepositoryContextsFactory repositoryServiceFactory; +// private CmsJcrDeployment jcrDeployment; + + @Override + public void start(BundleContext context) throws Exception { + bundleContext = context; + + // kernel thread + kernelThread = new StatisticsThread("Kernel Thread"); + kernelThread.setContextClassLoader(getClass().getClassLoader()); + kernelThread.start(); + + // JCR +// repositoryServiceFactory = new JackrabbitRepositoryContextsFactory(); +//// stopHooks.add(() -> repositoryServiceFactory.shutdown()); +// registerService(ManagedServiceFactory.class, repositoryServiceFactory, +// LangUtils.dict(Constants.SERVICE_PID, CmsConstants.NODE_REPOS_FACTORY_PID)); + +// JcrRepositoryFactory repositoryFactory = new JcrRepositoryFactory(); +// registerService(RepositoryFactory.class, repositoryFactory, null); + + // File System +// CmsJcrFsProvider cmsFsProvider = new CmsJcrFsProvider(); +// ServiceLoader fspSl = ServiceLoader.load(FileSystemProvider.class); +// for (FileSystemProvider fsp : fspSl) { +// log.debug("FileSystemProvider " + fsp); +// if (fsp instanceof CmsFsProvider) { +// cmsFsProvider = (CmsFsProvider) fsp; +// } +// } +// for (FileSystemProvider fsp : FileSystemProvider.installedProviders()) { +// log.debug("Installed FileSystemProvider " + fsp); +// } +// registerService(FileSystemProvider.class, cmsFsProvider, +// LangUtils.dict(Constants.SERVICE_PID, CmsConstants.NODE_FS_PROVIDER_PID)); + +// jcrDeployment = new CmsJcrDeployment(); +// jcrDeployment.init(); + } + + @Override + public void stop(BundleContext context) throws Exception { +// if (jcrDeployment != null) +// jcrDeployment.destroy(); + +// if (repositoryServiceFactory != null) +// repositoryServiceFactory.shutdown(); + + if (kernelThread != null) + kernelThread.destroyAndJoin(); + + bundleContext = null; + } + + @Deprecated + public static void registerService(Class clss, T service, Dictionary properties) { + if (bundleContext != null) { + bundleContext.registerService(clss, service, properties); + } + + } + + @Deprecated + public static BundleContext getBundleContext() { + return bundleContext; + } + + @Deprecated + public static T getService(Class clss) { + if (bundleContext != null) { + return bundleContext.getService(bundleContext.getServiceReference(clss)); + } else { + return null; + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsRemotingServlet.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsRemotingServlet.java new file mode 100644 index 0000000..fa3f87f --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsRemotingServlet.java @@ -0,0 +1,44 @@ +package org.argeo.cms.jcr.internal.servlet; + +import java.util.Map; + +import javax.jcr.Repository; + +import org.apache.jackrabbit.server.SessionProvider; +import org.apache.jackrabbit.server.remoting.davex.JcrRemotingServlet; +import org.argeo.api.cms.CmsConstants; + +/** A {@link JcrRemotingServlet} based on {@link CmsSessionProvider}. */ +public class CmsRemotingServlet extends JcrRemotingServlet { + private static final long serialVersionUID = 6459455509684213633L; + private Repository repository; + private SessionProvider sessionProvider; + + public CmsRemotingServlet() { + } + + public CmsRemotingServlet(String alias, Repository repository) { + this.repository = repository; + this.sessionProvider = new CmsSessionProvider(alias); + } + + @Override + public Repository getRepository() { + return repository; + } + + public void setRepository(Repository repository, Map properties) { + this.repository = repository; + String alias = properties.get(CmsConstants.CN); + if (alias != null) + sessionProvider = new CmsSessionProvider(alias); + else + throw new IllegalArgumentException("Only aliased repositories are supported"); + } + + @Override + protected SessionProvider getSessionProvider() { + return sessionProvider; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsSessionProvider.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsSessionProvider.java new file mode 100644 index 0000000..4e067ee --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsSessionProvider.java @@ -0,0 +1,175 @@ +package org.argeo.cms.jcr.internal.servlet; + +import java.io.Serializable; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.security.auth.Subject; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +import org.apache.jackrabbit.server.SessionProvider; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.CmsSession; +import org.argeo.cms.auth.RemoteAuthUtils; +import org.argeo.cms.servlet.ServletHttpRequest; +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 CmsLog log = CmsLog.getLog(CmsSessionProvider.class); + + private final String alias; + + private LinkedHashMap 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 { + + // a client is scanning parent URLs. +// if (workspace == null) +// return null; + + CmsSession cmsSession = RemoteAuthUtils.getCmsSession(new ServletHttpRequest(request)); + // CmsSessionImpl cmsSession = WebCmsSessionImpl.getCmsSession(request); + if (log.isTraceEnabled()) { + log.trace("Get JCR session from " + cmsSession); + } + if (cmsSession == null) + throw new IllegalStateException("Cannot find a session for request " + request.getRequestURI()); + CmsDataSession cmsDataSession = new CmsDataSession(cmsSession); + Session session = cmsDataSession.getDataSession(alias, workspace, rep); + cmsSessions.put(session, cmsDataSession); + return session; + } + + public void releaseSession(Session session) { +// JcrUtils.logoutQuietly(session); + if (cmsSessions.containsKey(session)) { + CmsDataSession cmsDataSession = cmsSessions.get(session); + cmsDataSession.releaseDataSession(alias, session); + } else { + log.warn("JCR session " + session + " not found in CMS session list. Logging it out..."); + JcrUtils.logoutQuietly(session); + } + } + + static class CmsDataSession { + private CmsSession cmsSession; + + private Map dataSessions = new HashMap<>(); + private Set dataSessionsInUse = new HashSet<>(); + private Set additionalDataSessions = new HashSet<>(); + + private CmsDataSession(CmsSession cmsSession) { + this.cmsSession = cmsSession; + cmsSession.addOnCloseCallback((sess) -> close()); + } + + public Session newDataSession(String cn, String workspace, Repository repository) { + checkValid(); + return login(repository, workspace); + } + + public synchronized Session getDataSession(String cn, String workspace, Repository repository) { + checkValid(); + // FIXME make it more robust + if (workspace == null) + workspace = CmsConstants.SYS_WORKSPACE; + 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 " + cmsSession.getUserDn()); + 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 " + cmsSession.getUserDn()); + } + dataSessionsInUse.add(path); + return session; + } + + private Session login(Repository repository, String workspace) { + try { + return Subject.doAs(cmsSession.getSubject(), new PrivilegedExceptionAction() { + @Override + public Session run() throws Exception { + return repository.login(workspace); + } + }); + } catch (PrivilegedActionException e) { + throw new IllegalStateException("Cannot log in " + cmsSession.getUserDn() + " 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 " + cmsSession.getUserDn()); + dataSessionsInUse.remove(path); + Session registeredSession = dataSessions.get(path); + if (session != registeredSession) + log.warn("Data session " + path + " not consistent for " + cmsSession.getUserDn()); + if (log.isTraceEnabled()) + log.trace("Released data session " + session + " for " + path); + notifyAll(); + } + + private void checkValid() { + if (!cmsSession.isValid()) + throw new IllegalStateException( + "CMS session " + cmsSession.getUuid() + " is not valid since " + cmsSession.getEnd()); + } + + protected void close() { + 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); + } + } + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsWebDavServlet.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsWebDavServlet.java new file mode 100644 index 0000000..0f0858f --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/CmsWebDavServlet.java @@ -0,0 +1,37 @@ +package org.argeo.cms.jcr.internal.servlet; + +import java.util.Map; + +import javax.jcr.Repository; + +import org.apache.jackrabbit.webdav.simple.SimpleWebdavServlet; +import org.argeo.api.cms.CmsConstants; + +/** A {@link SimpleWebdavServlet} based on {@link CmsSessionProvider}. */ +public class CmsWebDavServlet extends SimpleWebdavServlet { + private static final long serialVersionUID = 7485800288686328063L; + private Repository repository; + + public CmsWebDavServlet() { + } + + public CmsWebDavServlet(String alias, Repository repository) { + this.repository = repository; + setSessionProvider(new CmsSessionProvider(alias)); + } + + @Override + public Repository getRepository() { + return repository; + } + + public void setRepository(Repository repository, Map properties) { + this.repository = repository; + String alias = properties.get(CmsConstants.CN); + if (alias != null) + setSessionProvider(new CmsSessionProvider(alias)); + else + throw new IllegalArgumentException("Only aliased repositories are supported"); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/DataServletContext.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/DataServletContext.java new file mode 100644 index 0000000..2f60e97 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/DataServletContext.java @@ -0,0 +1,8 @@ +package org.argeo.cms.jcr.internal.servlet; + +import org.argeo.cms.servlet.CmsServletContext; + +/** Internal subclass, so that config resources can be loaded from our bundle. */ +public class DataServletContext extends CmsServletContext { + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrHttpUtils.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrHttpUtils.java new file mode 100644 index 0000000..11e903d --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrHttpUtils.java @@ -0,0 +1,73 @@ +package org.argeo.cms.jcr.internal.servlet; + +import java.util.Enumeration; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.argeo.api.cms.CmsLog; + +public class JcrHttpUtils { + 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/jcr/internal/servlet/protectedHandlers.xml"; + public final static String WEBDAV_CONFIG = "/org/argeo/cms/jcr/internal/servlet/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 logResponseHeaders(CmsLog log, HttpServletResponse response) { + if (!log.isDebugEnabled()) + return; + for (String headerName : response.getHeaderNames()) { + Object headerValue = response.getHeader(headerName); + log.debug(headerName + ": " + headerValue); + } + } + + public static void logRequestHeaders(CmsLog log, HttpServletRequest request) { + if (!log.isDebugEnabled()) + return; + for (Enumeration 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(CmsLog 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 en = request.getHeaderNames(); + while (en.hasMoreElements()) { + String header = en.nextElement(); + Enumeration values = request.getHeaders(header); + while (values.hasMoreElements()) + buf.append(" " + header + ": " + values.nextElement()); + buf.append('\n'); + } + + // attributed + Enumeration 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 JcrHttpUtils() { + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrReadServlet.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrReadServlet.java new file mode 100644 index 0000000..b0cd789 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrReadServlet.java @@ -0,0 +1,319 @@ +package org.argeo.cms.jcr.internal.servlet; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.AccessControlContext; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +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.nodetype.NodeType; +import javax.security.auth.Subject; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.api.JackrabbitNode; +import org.apache.jackrabbit.api.JackrabbitValue; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.integration.CmsExceptionsChain; +import org.argeo.jcr.JcrUtils; +import org.osgi.service.http.context.ServletContextHelper; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** Access a JCR repository via web services. */ +public class JcrReadServlet extends HttpServlet { + private static final long serialVersionUID = 6536175260540484539L; + private final static CmsLog log = CmsLog.getLog(JcrReadServlet.class); + + protected final static String ACCEPT_HTTP_HEADER = "Accept"; + protected final static String CONTENT_DISPOSITION_HTTP_HEADER = "Content-Disposition"; + + protected final static String OCTET_STREAM_CONTENT_TYPE = "application/octet-stream"; + protected final static String XML_CONTENT_TYPE = "application/xml"; + protected final static String JSON_CONTENT_TYPE = "application/json"; + + private final static String PARAM_VERBOSE = "verbose"; + private final static String PARAM_DEPTH = "depth"; + + protected final static String JCR_NODES = "jcr:nodes"; + // cf. javax.jcr.Property + protected final static String JCR_PATH = "path"; + protected final static String JCR_NAME = "name"; + + protected final static String _JCR = "_jcr"; + protected final static String JCR_PREFIX = "jcr:"; + protected final static String REP_PREFIX = "rep:"; + + private Repository repository; + private Integer maxDepth = 8; + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (log.isTraceEnabled()) + log.trace("Data service: " + req.getPathInfo()); + + String dataWorkspace = getWorkspace(req); + String jcrPath = getJcrPath(req); + + boolean verbose = req.getParameter(PARAM_VERBOSE) != null && !req.getParameter(PARAM_VERBOSE).equals("false"); + int depth = 1; + if (req.getParameter(PARAM_DEPTH) != null) { + depth = Integer.parseInt(req.getParameter(PARAM_DEPTH)); + if (depth > maxDepth) + throw new RuntimeException("Depth " + depth + " is higher than maximum " + maxDepth); + } + + Session session = null; + try { + // authentication + session = openJcrSession(req, resp, getRepository(), dataWorkspace); + if (!session.itemExists(jcrPath)) + throw new RuntimeException("JCR node " + jcrPath + " does not exist"); + Node node = session.getNode(jcrPath); + + List acceptHeader = readAcceptHeader(req); + if (!acceptHeader.isEmpty() && node.isNodeType(NodeType.NT_FILE)) { + resp.setContentType(OCTET_STREAM_CONTENT_TYPE); + resp.addHeader(CONTENT_DISPOSITION_HTTP_HEADER, "attachment; filename='" + node.getName() + "'"); + IOUtils.copy(JcrUtils.getFileAsStream(node), resp.getOutputStream()); + resp.flushBuffer(); + } else { + if (!acceptHeader.isEmpty() && acceptHeader.get(0).equals(XML_CONTENT_TYPE)) { + // TODO Use req.startAsync(); ? + resp.setContentType(XML_CONTENT_TYPE); + session.exportSystemView(node.getPath(), resp.getOutputStream(), false, depth <= 1); + return; + } + if (!acceptHeader.isEmpty() && !acceptHeader.contains(JSON_CONTENT_TYPE)) { + if (log.isTraceEnabled()) + log.warn("Content type " + acceptHeader + " in Accept header is not supported. Supported: " + + JSON_CONTENT_TYPE + " (default), " + XML_CONTENT_TYPE); + } + resp.setContentType(JSON_CONTENT_TYPE); + JsonGenerator jsonGenerator = getObjectMapper().getFactory().createGenerator(resp.getWriter()); + jsonGenerator.writeStartObject(); + writeNodeChildren(node, jsonGenerator, depth, verbose); + writeNodeProperties(node, jsonGenerator, verbose); + jsonGenerator.writeEndObject(); + jsonGenerator.flush(); + } + } catch (Exception e) { + new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp); + } finally { + JcrUtils.logoutQuietly(session); + } + } + + protected Session openJcrSession(HttpServletRequest req, HttpServletResponse resp, Repository repository, + String workspace) throws RepositoryException { + AccessControlContext acc = (AccessControlContext) req.getAttribute(ServletContextHelper.REMOTE_USER); + Subject subject = Subject.getSubject(acc); + try { + return Subject.doAs(subject, new PrivilegedExceptionAction() { + + @Override + public Session run() throws RepositoryException { + return repository.login(workspace); + } + + }); + } catch (PrivilegedActionException e) { + if (e.getException() instanceof RepositoryException) + throw (RepositoryException) e.getException(); + else + throw new RuntimeException(e.getException()); + } +// return workspace != null ? repository.login(workspace) : repository.login(); + } + + protected String getWorkspace(HttpServletRequest req) { + String path = req.getPathInfo(); + try { + path = URLDecoder.decode(path, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + String[] pathTokens = path.split("/"); + return pathTokens[1]; + } + + protected String getJcrPath(HttpServletRequest req) { + String path = req.getPathInfo(); + try { + path = URLDecoder.decode(path, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + String[] pathTokens = path.split("/"); + String domain = pathTokens[1]; + String jcrPath = path.substring(domain.length() + 1); + return jcrPath; + } + + protected List readAcceptHeader(HttpServletRequest req) { + List lst = new ArrayList<>(); + String acceptHeader = req.getHeader(ACCEPT_HTTP_HEADER); + if (acceptHeader == null) + return lst; +// Enumeration acceptHeader = req.getHeaders(ACCEPT_HTTP_HEADER); +// while (acceptHeader.hasMoreElements()) { + String[] arr = acceptHeader.split("\\."); + for (int i = 0; i < arr.length; i++) { + String str = arr[i].trim(); + if (!"".equals(str)) + lst.add(str); + } +// } + return lst; + } + + protected void writeNodeProperties(Node node, JsonGenerator jsonGenerator, boolean verbose) + throws RepositoryException, IOException { + String jcrPath = node.getPath(); + Map> namespaces = new TreeMap<>(); + + PropertyIterator pit = node.getProperties(); + properties: while (pit.hasNext()) { + Property property = pit.nextProperty(); + + final String propertyName = property.getName(); + int columnIndex = propertyName.indexOf(':'); + if (columnIndex > 0) { + // mark prefix with a '_' before the name of the object, according to JSON + // conventions to indicate a special value + String prefix = "_" + propertyName.substring(0, columnIndex); + String unqualifiedName = propertyName.substring(columnIndex + 1); + if (!namespaces.containsKey(prefix)) + namespaces.put(prefix, new LinkedHashMap()); + Map map = namespaces.get(prefix); + assert !map.containsKey(unqualifiedName); + map.put(unqualifiedName, property); + continue properties; + } + + if (property.getType() == PropertyType.BINARY) { + if (!(node instanceof JackrabbitNode)) { + continue properties;// skip + } + } + + writeProperty(propertyName, property, jsonGenerator); + } + + for (String prefix : namespaces.keySet()) { + Map map = namespaces.get(prefix); + jsonGenerator.writeFieldName(prefix); + jsonGenerator.writeStartObject(); + if (_JCR.equals(prefix)) { + jsonGenerator.writeStringField(JCR_NAME, node.getName()); + jsonGenerator.writeStringField(JCR_PATH, jcrPath); + } + properties: for (String unqualifiedName : map.keySet()) { + Property property = map.get(unqualifiedName); + if (property.getType() == PropertyType.BINARY) { + if (!(node instanceof JackrabbitNode)) { + continue properties;// skip + } + } + writeProperty(unqualifiedName, property, jsonGenerator); + } + jsonGenerator.writeEndObject(); + } + } + + protected void writeProperty(String fieldName, Property property, JsonGenerator jsonGenerator) + throws RepositoryException, IOException { + if (!property.isMultiple()) { + jsonGenerator.writeFieldName(fieldName); + writePropertyValue(property.getType(), property.getValue(), jsonGenerator); + } else { + jsonGenerator.writeFieldName(fieldName); + jsonGenerator.writeStartArray(); + Value[] values = property.getValues(); + for (Value value : values) { + writePropertyValue(property.getType(), value, jsonGenerator); + } + jsonGenerator.writeEndArray(); + } + } + + protected void writePropertyValue(int type, Value value, JsonGenerator jsonGenerator) + throws RepositoryException, IOException { + if (type == PropertyType.DOUBLE) + jsonGenerator.writeNumber(value.getDouble()); + else if (type == PropertyType.LONG) + jsonGenerator.writeNumber(value.getLong()); + else if (type == PropertyType.BINARY) { + if (value instanceof JackrabbitValue) { + String contentIdentity = ((JackrabbitValue) value).getContentIdentity(); + jsonGenerator.writeString("SHA256:" + contentIdentity); + } else { + // TODO write Base64 ? + jsonGenerator.writeNull(); + } + } else + jsonGenerator.writeString(value.getString()); + } + + protected void writeNodeChildren(Node node, JsonGenerator jsonGenerator, int depth, boolean verbose) + throws RepositoryException, IOException { + if (!node.hasNodes()) + return; + if (depth <= 0) + return; + NodeIterator nit; + + nit = node.getNodes(); + children: while (nit.hasNext()) { + Node child = nit.nextNode(); + if (!verbose && child.getName().startsWith(REP_PREFIX)) { + continue children;// skip Jackrabbit auth metadata + } + + jsonGenerator.writeFieldName(child.getName()); + jsonGenerator.writeStartObject(); + writeNodeChildren(child, jsonGenerator, depth - 1, verbose); + writeNodeProperties(child, jsonGenerator, verbose); + jsonGenerator.writeEndObject(); + } + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public void setMaxDepth(Integer maxDepth) { + this.maxDepth = maxDepth; + } + + protected Repository getRepository() { + return repository; + } + + protected ObjectMapper getObjectMapper() { + return objectMapper; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrServletContext.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrServletContext.java new file mode 100644 index 0000000..21046f3 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrServletContext.java @@ -0,0 +1,8 @@ +package org.argeo.cms.jcr.internal.servlet; + +import org.argeo.cms.servlet.PrivateWwwAuthServletContext; + +/** Internal subclass, so that config resources can be loaded from our bundle. */ +public class JcrServletContext extends PrivateWwwAuthServletContext { + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrWriteServlet.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrWriteServlet.java new file mode 100644 index 0000000..459a1e4 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/JcrWriteServlet.java @@ -0,0 +1,92 @@ +package org.argeo.cms.jcr.internal.servlet; + +import java.io.IOException; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Node; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.integration.CmsExceptionsChain; +import org.argeo.jcr.JcrUtils; + +/** Access a JCR repository via web services. */ +public class JcrWriteServlet extends JcrReadServlet { + private static final long serialVersionUID = 17272653843085492L; + private final static CmsLog log = CmsLog.getLog(JcrWriteServlet.class); + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (log.isDebugEnabled()) + log.debug("Data service POST: " + req.getPathInfo()); + + String dataWorkspace = getWorkspace(req); + String jcrPath = getJcrPath(req); + + Session session = null; + try { + // authentication + session = openJcrSession(req, resp, getRepository(), dataWorkspace); + + if (req.getContentType() != null && req.getContentType().equals(XML_CONTENT_TYPE)) { +// resp.setContentType(XML_CONTENT_TYPE); + session.getWorkspace().importXML(jcrPath, req.getInputStream(), + ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING); + return; + } + + if (!session.itemExists(jcrPath)) { + String parentPath = FilenameUtils.getFullPathNoEndSeparator(jcrPath); + String fileName = FilenameUtils.getName(jcrPath); + Node folderNode = JcrUtils.mkfolders(session, parentPath); + byte[] bytes = IOUtils.toByteArray(req.getInputStream()); + JcrUtils.copyBytesAsFile(folderNode, fileName, bytes); + } else { + Node node = session.getNode(jcrPath); + if (!node.isNodeType(NodeType.NT_FILE)) + throw new IllegalArgumentException("Node " + jcrPath + " exists but is not a file"); + byte[] bytes = IOUtils.toByteArray(req.getInputStream()); + JcrUtils.copyBytesAsFile(node.getParent(), node.getName(), bytes); + } + session.save(); + } catch (Exception e) { + new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp); + } finally { + JcrUtils.logoutQuietly(session); + } + } + + @Override + protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (log.isDebugEnabled()) + log.debug("Data service DELETE: " + req.getPathInfo()); + + String dataWorkspace = getWorkspace(req); + String jcrPath = getJcrPath(req); + + Session session = null; + try { + // authentication + session = openJcrSession(req, resp, getRepository(), dataWorkspace); + if (!session.itemExists(jcrPath)) { + // ignore + return; + } else { + Node node = session.getNode(jcrPath); + node.remove(); + } + session.save(); + } catch (Exception e) { + new CmsExceptionsChain(e).writeAsJson(getObjectMapper(), resp); + } finally { + JcrUtils.logoutQuietly(session); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/LinkServlet.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/LinkServlet.java new file mode 100644 index 0000000..be5684a --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/LinkServlet.java @@ -0,0 +1,258 @@ +package org.argeo.cms.jcr.internal.servlet; + +import static javax.jcr.Property.JCR_DESCRIPTION; +import static javax.jcr.Property.JCR_LAST_MODIFIED; +import static javax.jcr.Property.JCR_TITLE; + +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.api.cms.CmsAuth; +import org.argeo.api.cms.CmsConstants; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.jcr.JcrUtils; +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() { + + @Override + public Session run() throws Exception { + Collection> srs = bc.getServiceReferences(Repository.class, + "(" + CmsConstants.CN + "=" + CmsConstants.EGO_REPOSITORY + ")"); + 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; + // TODO support images +// 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(""); + buf.append(""); + writeMeta(buf, "og:title", escapeHTML(title)); + writeMeta(buf, "og:type", "website"); + buf.append(""); + buf.append(""); + 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(""); + buf.append(""); + buf.append("

!! This page is meant for indexing robots, not for real people," + " visit ").append(escapeHTML(title)).append(" instead.

"); + writeCanonical(buf, node); + buf.append(""); + buf.append(""); + writer.print(buf.toString()); + + response.setHeader("Content-Type", "text/html"); + writer.flush(); + } catch (Exception e) { + throw new IllegalStateException("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(""); + } + + private void writeCanonical(StringBuilder buf, Node node) throws RepositoryException { + buf.append("
"); + if (node.hasProperty(JCR_TITLE)) + buf.append("

").append(node.getProperty(JCR_TITLE).getString()).append("

"); + if (node.hasProperty(JCR_DESCRIPTION)) + buf.append("

").append(node.getProperty(JCR_DESCRIPTION).getString()).append("

"); + NodeIterator children = node.getNodes(); + while (children.hasNext()) { + writeCanonical(buf, children.nextNode()); + } + buf.append("
"); + } + + // 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 IllegalArgumentException("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(CmsJcrUtils.getDataPath(CmsConstants.EGO_REPOSITORY, node)); + return new URL(buf.toString()).toString(); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("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 IllegalArgumentException("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(CmsAuth.LOGIN_CONTEXT_ANONYMOUS, subject); + lc.login(); + return subject; + } catch (LoginException e) { + throw new IllegalStateException("Cannot login as anonymous", e); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/protectedHandlers.xml b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/protectedHandlers.xml new file mode 100644 index 0000000..59f22cd --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/protectedHandlers.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/webdav-config.xml b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/webdav-config.xml new file mode 100644 index 0000000..4363898 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/internal/servlet/webdav-config.xml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nt:file + nt:resource + + + + + + + + + + + + + rep + jcr + + node + argeo + cms + slc + connect + activities + people + documents + tracker + + + + + + + diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/ldap.cnd b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/ldap.cnd new file mode 100644 index 0000000..a2306c6 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/ldap.cnd @@ -0,0 +1 @@ + diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/node.cnd b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/node.cnd new file mode 100644 index 0000000..d8a26b6 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/node.cnd @@ -0,0 +1,9 @@ + + +[node:userHome] +mixin +- ldap:uid (STRING) m + +[node:groupHome] +mixin +- ldap:cn (STRING) m diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/CsvTabularWriter.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/CsvTabularWriter.java new file mode 100644 index 0000000..ccd543f --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/CsvTabularWriter.java @@ -0,0 +1,23 @@ +package org.argeo.cms.jcr.tabular; + +import java.io.OutputStream; + +import org.argeo.cms.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.jcr/src/org/argeo/cms/jcr/tabular/JcrTabularRowIterator.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/JcrTabularRowIterator.java new file mode 100644 index 0000000..d1d9b58 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/JcrTabularRowIterator.java @@ -0,0 +1,170 @@ +package org.argeo.cms.jcr.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.cms.tabular.ArrayTabularRow; +import org.argeo.cms.tabular.TabularColumn; +import org.argeo.cms.tabular.TabularRow; +import org.argeo.cms.tabular.TabularRowIterator; +import org.argeo.jcr.JcrException; +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 header = new ArrayList(); + + /** referenced so that we can close it */ + private Binary binary; + private InputStream in; + + private CsvParser csvParser; + private ArrayBlockingQueue> 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>(1000); + csvParser = new CsvParser() { + protected void processLine(Integer lineNumber, + List header, List 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 JcrException("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 tokens = textLines.take(); + List objs = new ArrayList(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 getHeader() { + return header; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/JcrTabularWriter.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/JcrTabularWriter.java new file mode 100644 index 0000000..cc3e0d7 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/JcrTabularWriter.java @@ -0,0 +1,82 @@ +package org.argeo.cms.jcr.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.cms.tabular.TabularColumn; +import org.argeo.cms.tabular.TabularWriter; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +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 columns; + + /** Creates a table node */ + public JcrTabularWriter(Node tableNode, List 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 JcrException("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 JcrException("Cannot store data in " + contentNode, e); + } finally { + IOUtils.closeQuietly(in); + JcrUtils.closeQuietly(binary); + } + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/package-info.java b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/package-info.java new file mode 100644 index 0000000..506a6ac --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/cms/jcr/tabular/package-info.java @@ -0,0 +1,2 @@ +/** Argeo CMS implementation of the Argeo Tabular API (CSV, JCR). */ +package org.argeo.cms.jcr.tabular; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java new file mode 100644 index 0000000..7396c87 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitAdminLoginModule.java @@ -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 sharedState, Map 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.cms.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java new file mode 100644 index 0000000..8c267e3 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/JackrabbitDataModelMigration.java @@ -0,0 +1,172 @@ +package org.argeo.jackrabbit; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.commons.cnd.CndImporter; +import org.apache.jackrabbit.commons.cnd.ParseException; +import org.apache.jackrabbit.core.config.RepositoryConfig; +import org.apache.jackrabbit.core.fs.FileSystemException; +import org.argeo.api.cms.CmsLog; +import org.argeo.jcr.JcrCallback; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; + +/** Migrate the data in a Jackrabbit repository. */ +@Deprecated +public class JackrabbitDataModelMigration implements Comparable { + private final static CmsLog log = CmsLog.getLog(JackrabbitDataModelMigration.class); + + private String dataModelNodePath; + private String targetVersion; + private URL 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.openStream()); + 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 (RepositoryException e) { + JcrUtils.discardQuietly(session); + throw new JcrException("Migration of data model " + dataModelNodePath + " to " + targetVersion + " failed.", + e); + } catch (ParseException | IOException e) { + JcrUtils.discardQuietly(session); + throw new RuntimeException( + "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 (RuntimeException e) { + throw e; + } catch (RepositoryException e) { + throw new JcrException(e); + } catch (FileSystemException e) { + throw new RuntimeException("Cannot clear node types cache.",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(URL 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.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java new file mode 100644 index 0000000..77ad527 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryFactory.java @@ -0,0 +1,26 @@ +package org.argeo.jackrabbit.client; + +import java.util.Map; + +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.RepositoryFactory; + +import org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory; +import org.apache.jackrabbit.jcr2spi.RepositoryImpl; +import org.apache.jackrabbit.spi.RepositoryServiceFactory; + +/** A customised {@link RepositoryFactory} access a remote DAVEX service. */ +public class ClientDavexRepositoryFactory implements RepositoryFactory { + public final static String JACKRABBIT_DAVEX_URI = ClientDavexRepositoryServiceFactory.PARAM_REPOSITORY_URI; + public final static String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = ClientDavexRepositoryServiceFactory.PARAM_WORKSPACE_NAME_DEFAULT; + + @SuppressWarnings("rawtypes") + @Override + public Repository getRepository(Map parameters) throws RepositoryException { + RepositoryServiceFactory repositoryServiceFactory = new ClientDavexRepositoryServiceFactory(); + return RepositoryImpl + .create(new Jcr2spiRepositoryFactory.RepositoryConfigImpl(repositoryServiceFactory, parameters)); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java new file mode 100644 index 0000000..7d86af2 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryService.java @@ -0,0 +1,51 @@ +package org.argeo.jackrabbit.client; + +import javax.jcr.RepositoryException; + +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi2davex.BatchReadConfig; +import org.apache.jackrabbit.spi2davex.RepositoryServiceImpl; + +/** + * Wrapper for {@link RepositoryServiceImpl} in order to access the underlying + * {@link HttpClientContext}. + */ +public class ClientDavexRepositoryService extends RepositoryServiceImpl { + + public ClientDavexRepositoryService(String jcrServerURI, BatchReadConfig batchReadConfig) + throws RepositoryException { + super(jcrServerURI, batchReadConfig); + } + + + +// public ClientDavexRepositoryService(String jcrServerURI, String defaultWorkspaceName, +// BatchReadConfig batchReadConfig, int itemInfoCacheSize, ConnectionOptions connectionOptions) +// throws RepositoryException { +// super(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize, connectionOptions); +// // TODO Auto-generated constructor stub +// } + + + +// public ClientDavexRepositoryService(String jcrServerURI, String defaultWorkspaceName, +// BatchReadConfig batchReadConfig, int itemInfoCacheSize, int maximumHttpConnections) +// throws RepositoryException { +// super(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize, maximumHttpConnections); +// } +// +// public ClientDavexRepositoryService(String jcrServerURI, String defaultWorkspaceName, +// BatchReadConfig batchReadConfig, int itemInfoCacheSize) throws RepositoryException { +// super(jcrServerURI, defaultWorkspaceName, batchReadConfig, itemInfoCacheSize); +// } + + @Override + protected HttpContext getContext(SessionInfo sessionInfo) throws RepositoryException { + HttpClientContext result = HttpClientContext.create(); + result.setAuthCache(new NonSerialBasicAuthCache()); + return result; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java new file mode 100644 index 0000000..2af0835 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/ClientDavexRepositoryServiceFactory.java @@ -0,0 +1,84 @@ +package org.argeo.jackrabbit.client; + +import java.util.Map; + +import javax.jcr.RepositoryException; + +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; +import org.apache.jackrabbit.spi2davex.BatchReadConfig; +import org.apache.jackrabbit.spi2davex.Spi2davexRepositoryServiceFactory; + +/** + * Wrapper for {@link Spi2davexRepositoryServiceFactory} in order to create a + * {@link ClientDavexRepositoryService}. + */ +public class ClientDavexRepositoryServiceFactory extends Spi2davexRepositoryServiceFactory { + @Override + public RepositoryService createRepositoryService(Map parameters) throws RepositoryException { + // retrieve the repository uri + String uri; + if (parameters == null) { + uri = System.getProperty(PARAM_REPOSITORY_URI); + } else { + Object repoUri = parameters.get(PARAM_REPOSITORY_URI); + uri = (repoUri == null) ? null : repoUri.toString(); + } + if (uri == null) { + uri = DEFAULT_REPOSITORY_URI; + } + + // load other optional configuration parameters + BatchReadConfig brc = null; + int itemInfoCacheSize = ItemInfoCacheImpl.DEFAULT_CACHE_SIZE; + int maximumHttpConnections = 0; + + // since JCR-4120 the default workspace name is no longer set to 'default' + // note: if running with JCR Server < 1.5 a default workspace name must + // therefore be configured + String workspaceNameDefault = null; + + if (parameters != null) { + // batchRead config + Object param = parameters.get(PARAM_BATCHREAD_CONFIG); + if (param != null && param instanceof BatchReadConfig) { + brc = (BatchReadConfig) param; + } + + // itemCache size config + param = parameters.get(PARAM_ITEMINFO_CACHE_SIZE); + if (param != null) { + try { + itemInfoCacheSize = Integer.parseInt(param.toString()); + } catch (NumberFormatException e) { + // ignore, use default + } + } + + // max connections config + param = parameters.get(PARAM_MAX_CONNECTIONS); + if (param != null) { + try { + maximumHttpConnections = Integer.parseInt(param.toString()); + } catch (NumberFormatException e) { + // using default + } + } + + param = parameters.get(PARAM_WORKSPACE_NAME_DEFAULT); + if (param != null) { + workspaceNameDefault = param.toString(); + } + } + + // FIXME adapt to changes in Jackrabbit +// if (maximumHttpConnections > 0) { +// return new ClientDavexRepositoryService(uri, workspaceNameDefault, brc, itemInfoCacheSize, +// maximumHttpConnections); +// } else { +// return new ClientDavexRepositoryService(uri, workspaceNameDefault, brc, itemInfoCacheSize); +// } + return null; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/JackrabbitClient.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/JackrabbitClient.java new file mode 100644 index 0000000..3a122f1 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/JackrabbitClient.java @@ -0,0 +1,127 @@ +package org.argeo.jackrabbit.client; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +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 org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.protocol.HttpContext; +import org.apache.jackrabbit.jcr2dav.Jcr2davRepositoryFactory; +import org.apache.jackrabbit.jcr2spi.Jcr2spiRepositoryFactory; +import org.apache.jackrabbit.jcr2spi.RepositoryImpl; +import org.apache.jackrabbit.spi.RepositoryService; +import org.apache.jackrabbit.spi.RepositoryServiceFactory; +import org.apache.jackrabbit.spi.SessionInfo; +import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl; +import org.apache.jackrabbit.spi2davex.BatchReadConfig; +import org.apache.jackrabbit.spi2davex.RepositoryServiceImpl; +import org.apache.jackrabbit.spi2davex.Spi2davexRepositoryServiceFactory; +import org.argeo.jcr.JcrUtils; + +/** Minimal client to test JCR DAVEX connectivity. */ +public class JackrabbitClient { + final static String JACKRABBIT_REPOSITORY_URI = "org.apache.jackrabbit.repository.uri"; + final static String JACKRABBIT_DAVEX_URI = "org.apache.jackrabbit.spi2davex.uri"; + final static String JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "org.apache.jackrabbit.spi2davex.WorkspaceNameDefault"; + + public static void main(String[] args) { + String repoUri = args.length == 0 ? "http://root:demo@localhost:7070/jcr/ego" : args[0]; + String workspace = args.length < 2 ? "home" : args[1]; + + Repository repository = null; + Session session = null; + + URI uri; + try { + uri = new URI(repoUri); + } catch (URISyntaxException e1) { + throw new IllegalArgumentException(e1); + } + + if (uri.getScheme().equals("http") || uri.getScheme().equals("https")) { + + RepositoryFactory repositoryFactory = new Jcr2davRepositoryFactory() { + @SuppressWarnings("rawtypes") + public Repository getRepository(Map parameters) throws RepositoryException { + RepositoryServiceFactory repositoryServiceFactory = new Spi2davexRepositoryServiceFactory() { + + @Override + public RepositoryService createRepositoryService(Map parameters) + throws RepositoryException { + Object uri = parameters.get(JACKRABBIT_DAVEX_URI); + Object defaultWorkspace = parameters.get(JACKRABBIT_REMOTE_DEFAULT_WORKSPACE); + BatchReadConfig brc = null; + // FIXME adapt to change in Jackrabbit +// return new RepositoryServiceImpl(uri.toString(), defaultWorkspace.toString(), brc, +// ItemInfoCacheImpl.DEFAULT_CACHE_SIZE) { +// +// @Override +// protected HttpContext getContext(SessionInfo sessionInfo) throws RepositoryException { +// HttpClientContext result = HttpClientContext.create(); +// result.setAuthCache(new NonSerialBasicAuthCache()); +// return result; +// } +// +// }; + return null; + } + }; + return RepositoryImpl.create( + new Jcr2spiRepositoryFactory.RepositoryConfigImpl(repositoryServiceFactory, parameters)); + } + }; + Map params = new HashMap(); + params.put(JACKRABBIT_DAVEX_URI, repoUri.toString()); + // FIXME make it configurable + params.put(JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, "sys"); + + try { + repository = repositoryFactory.getRepository(params); + if (repository != null) + session = repository.login(workspace); + else + throw new IllegalArgumentException("Repository " + repoUri + " not found"); + } catch (RepositoryException e) { + e.printStackTrace(); + } + + } else { + Path path = Paths.get(uri.getPath()); + } + + try { + Node rootNode = session.getRootNode(); + NodeIterator nit = rootNode.getNodes(); + while (nit.hasNext()) { + System.out.println(nit.nextNode().getPath()); + } + + Node newNode = JcrUtils.mkdirs(rootNode, "dir/subdir"); + System.out.println("Created folder " + newNode.getPath()); + Node newFile = JcrUtils.copyBytesAsFile(newNode, "test.txt", "TEST".getBytes()); + System.out.println("Created file " + newFile.getPath()); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(JcrUtils.getFileAsStream(newFile)))) { + System.out.println("Read " + reader.readLine()); + } catch (IOException e) { + e.printStackTrace(); + } + newNode.getParent().remove(); + System.out.println("Removed new nodes"); + } catch (RepositoryException e) { + e.printStackTrace(); + } + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java new file mode 100644 index 0000000..3fb0db9 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/client/NonSerialBasicAuthCache.java @@ -0,0 +1,41 @@ +package org.argeo.jackrabbit.client; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScheme; +import org.apache.http.client.AuthCache; + +/** + * Implementation of {@link AuthCache} which doesn't use serialization, as it is + * not supported by GraalVM at this stage. + */ +public class NonSerialBasicAuthCache implements AuthCache { + private final Map cache; + + public NonSerialBasicAuthCache() { + cache = new ConcurrentHashMap(); + } + + @Override + public void put(HttpHost host, AuthScheme authScheme) { + cache.put(host, authScheme); + } + + @Override + public AuthScheme get(HttpHost host) { + return cache.get(host); + } + + @Override + public void remove(HttpHost host) { + cache.remove(host); + } + + @Override + public void clear() { + cache.clear(); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java new file mode 100644 index 0000000..a2eb983 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/AbstractJackrabbitFsProvider.java @@ -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.cms.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java new file mode 100644 index 0000000..1cae6e4 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java @@ -0,0 +1,149 @@ +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.argeo.jackrabbit.client.ClientDavexRepositoryFactory; +import org.argeo.jcr.fs.JcrFileSystem; +import org.argeo.jcr.fs.JcrFsException; + +/** + * A file system provider based on a JCR repository remotely accessed via the + * DAVEX protocol. + */ +public class DavexFsProvider extends AbstractJackrabbitFsProvider { + final static String DEFAULT_JACKRABBIT_REMOTE_DEFAULT_WORKSPACE = "sys"; + + private Map fileSystems = new HashMap<>(); + + @Override + public String getScheme() { + return "davex"; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + if (uri.getHost() == null) + throw new IllegalArgumentException("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 ClientDavexRepositoryFactory(); + return tryGetRepo(repositoryFactory, repoUri, "home"); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot open file system " + uri, e); + } + } + + private JcrFileSystem tryGetRepo(RepositoryFactory repositoryFactory, URI repoUri, String workspace) + throws IOException { + Map params = new HashMap(); + params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, repoUri.toString()); + // TODO better integrate with OSGi or other configuration than system + // properties. + String remoteDefaultWorkspace = System.getProperty( + ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, + DEFAULT_JACKRABBIT_REMOTE_DEFAULT_WORKSPACE); + params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, remoteDefaultWorkspace); + 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 IllegalArgumentException("Badly formatted URI", e); + } + return tryGetRepo(repositoryFactory, nextUri, nextWorkspace); + } else { + JcrFileSystem fileSystem = new JcrFileSystem(this, repository); + 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()); + if (fileSystem == null) + throw new IllegalArgumentException("No file system found for " + uri); + } 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) { + throw new IllegalArgumentException(e); + } + String uriStr = repoUri.toString(); + String localPath = null; + for (String key : fileSystems.keySet()) { + if (uriStr.startsWith(key)) { + localPath = uriStr.toString().substring(key.length()); + } + } + if ("".equals(localPath)) + localPath = "/"; + 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/ego/")); + System.out.println(path); + DirectoryStream ds = Files.newDirectoryStream(path); + for (Path p : ds) { + System.out.println("- " + p); + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java new file mode 100644 index 0000000..e3a70d0 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/JackrabbitMemoryFsProvider.java @@ -0,0 +1,87 @@ +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.Credentials; +import javax.jcr.Repository; +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; + + private Credentials credentials; + + public JackrabbitMemoryFsProvider() { + String username = System.getProperty("user.name"); + credentials = new SimpleCredentials(username, username.toCharArray()); + } + + @Override + public String getScheme() { + return "jcr+memory"; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + try { + Path tempDir = Files.createTempDirectory("fs-memory"); + URL confUrl = JackrabbitMemoryFsProvider.class.getResource("fs-memory.xml"); + RepositoryConfig repositoryConfig = RepositoryConfig.create(confUrl.toURI(), tempDir.toString()); + repository = RepositoryImpl.create(repositoryConfig); + postRepositoryCreation(repository); + fileSystem = new JcrFileSystem(this, repository, credentials); + 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()); + } catch (IOException e) { + throw new JcrFsException("Could not autocreate file system", e); + } + return fileSystem.getPath(path); + } + + public Repository getRepository() { + return repository; + } + + public Session login() throws RepositoryException { + return getRepository().login(credentials); + } + + /** + * Called after the repository has been created and before the file system is + * created. + */ + protected void postRepositoryCreation(RepositoryImpl repositoryImpl) throws RepositoryException { + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml new file mode 100644 index 0000000..f2541fb --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/fs-memory.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/package-info.java new file mode 100644 index 0000000..c9ec2c3 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/fs/package-info.java @@ -0,0 +1,2 @@ +/** Java NIO file system implementation based on Jackrabbit. */ +package org.argeo.jackrabbit.fs; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/package-info.java new file mode 100644 index 0000000..17497d6 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/package-info.java @@ -0,0 +1,2 @@ +/** Generic Jackrabbit utilities. */ +package org.argeo.jackrabbit; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-h2.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-h2.xml new file mode 100644 index 0000000..0526762 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-h2.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-localfs.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-localfs.xml new file mode 100644 index 0000000..3d24708 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-localfs.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-memory.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-memory.xml new file mode 100644 index 0000000..ecee5bd --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-memory.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml new file mode 100644 index 0000000..07a0d04 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql-ds.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml new file mode 100644 index 0000000..9677828 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/repository-postgresql.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java new file mode 100644 index 0000000..f98cf99 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/JackrabbitSecurityUtils.java @@ -0,0 +1,79 @@ +package org.argeo.jackrabbit.security; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.security.Privilege; + +import org.apache.jackrabbit.api.security.JackrabbitAccessControlList; +import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager; +import org.argeo.api.cms.CmsLog; +import org.argeo.jcr.JcrUtils; + +/** Utilities around Jackrabbit security extensions. */ +public class JackrabbitSecurityUtils { + private final static CmsLog log = CmsLog.getLog(JackrabbitSecurityUtils.class); + + /** + * Convenience method for denying a single privilege to a principal (user or + * role), typically jcr:all + */ + public synchronized static void denyPrivilege(Session session, String path, String principal, String privilege) + throws RepositoryException { + List privileges = new ArrayList(); + privileges.add(session.getAccessControlManager().privilegeFromName(privilege)); + denyPrivileges(session, path, () -> principal, privileges); + } + + /** + * Deny 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 denyPrivileges(Session session, String path, Principal principal, + List privs) throws RepositoryException { + // make sure the session is in line with the persisted state + session.refresh(false); + JackrabbitAccessControlManager acm = (JackrabbitAccessControlManager) session.getAccessControlManager(); + JackrabbitAccessControlList acl = (JackrabbitAccessControlList) JcrUtils.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.addEntry(principal, privileges, false); + acm.setPolicy(path, acl); + if (log.isDebugEnabled()) { + StringBuffer privBuf = new StringBuffer(); + for (Privilege priv : privs) + privBuf.append(priv.getName()); + log.debug("Denied privileges " + privBuf + " to " + principal.getName() + " on " + path + " in '" + + session.getWorkspace().getName() + "'"); + } + session.refresh(true); + session.save(); + return true; + } + + /** Singleton. */ + private JackrabbitSecurityUtils() { + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/package-info.java new file mode 100644 index 0000000..f3a282c --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jackrabbit/security/package-info.java @@ -0,0 +1,2 @@ +/** Generic Jackrabbit security utilities. */ +package org.argeo.jackrabbit.security; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/Bin.java b/org.argeo.cms.jcr/src/org/argeo/jcr/Bin.java new file mode 100644 index 0000000..0418810 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/Bin.java @@ -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.cms.jcr/src/org/argeo/jcr/CollectionNodeIterator.java b/org.argeo.cms.jcr/src/org/argeo/jcr/CollectionNodeIterator.java new file mode 100644 index 0000000..b4124ee --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/CollectionNodeIterator.java @@ -0,0 +1,61 @@ +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 iterator; + private Integer position = 0; + + public CollectionNodeIterator(Collection 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.cms.jcr/src/org/argeo/jcr/DefaultJcrListener.java b/org.argeo.cms.jcr/src/org/argeo/jcr/DefaultJcrListener.java new file mode 100644 index 0000000..d873ef6 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/DefaultJcrListener.java @@ -0,0 +1,77 @@ +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.argeo.api.cms.CmsLog; + +/** To be overridden */ +public class DefaultJcrListener implements EventListener { + private final static CmsLog log = CmsLog.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 (RepositoryException e) { + throw new JcrException("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 (RepositoryException e) { + throw new JcrException("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.cms.jcr/src/org/argeo/jcr/Jcr.java b/org.argeo.cms.jcr/src/org/argeo/jcr/Jcr.java new file mode 100644 index 0000000..49b008d --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/Jcr.java @@ -0,0 +1,993 @@ +package org.argeo.jcr; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.text.MessageFormat; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +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 javax.jcr.Binary; +import javax.jcr.ItemNotFoundException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +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.query.Query; +import javax.jcr.query.QueryManager; +import javax.jcr.query.Row; +import javax.jcr.security.Privilege; +import javax.jcr.version.Version; +import javax.jcr.version.VersionHistory; +import javax.jcr.version.VersionIterator; +import javax.jcr.version.VersionManager; + +import org.apache.commons.io.IOUtils; + +/** + * Utility class whose purpose is to make using JCR less verbose by + * systematically using unchecked exceptions and returning null + * when something is not found. This is especially useful when writing user + * interfaces (such as with SWT) where listeners and callbacks expect unchecked + * exceptions. Loosely inspired by Java's Files singleton. + */ +public class Jcr { + /** + * The name of a node which will be serialized as XML text, as per section 7.3.1 + * of the JCR 2.0 specifications. + */ + public final static String JCR_XMLTEXT = "jcr:xmltext"; + /** + * The name of a property which will be serialized as XML text, as per section + * 7.3.1 of the JCR 2.0 specifications. + */ + public final static String JCR_XMLCHARACTERS = "jcr:xmlcharacters"; + /** + * jcr:name, when used in another context than + * {@link Property#JCR_NAME}, typically to name a node rather than a property. + */ + public final static String JCR_NAME = "jcr:name"; + /** + * jcr:path, when used in another context than + * {@link Property#JCR_PATH}, typically to name a node rather than a property. + */ + public final static String JCR_PATH = "jcr:path"; + /** + * jcr:primaryType with prefix instead of namespace (as in + * {@link Property#JCR_PRIMARY_TYPE}. + */ + public final static String JCR_PRIMARY_TYPE = "jcr:primaryType"; + /** + * jcr:mixinTypes with prefix instead of namespace (as in + * {@link Property#JCR_MIXIN_TYPES}. + */ + public final static String JCR_MIXIN_TYPES = "jcr:mixinTypes"; + /** + * jcr:uuid with prefix instead of namespace (as in + * {@link Property#JCR_UUID}. + */ + public final static String JCR_UUID = "jcr:uuid"; + /** + * jcr:created with prefix instead of namespace (as in + * {@link Property#JCR_CREATED}. + */ + public final static String JCR_CREATED = "jcr:created"; + /** + * jcr:createdBy with prefix instead of namespace (as in + * {@link Property#JCR_CREATED_BY}. + */ + public final static String JCR_CREATED_BY = "jcr:createdBy"; + /** + * jcr:lastModified with prefix instead of namespace (as in + * {@link Property#JCR_LAST_MODIFIED}. + */ + public final static String JCR_LAST_MODIFIED = "jcr:lastModified"; + /** + * jcr:lastModifiedBy with prefix instead of namespace (as in + * {@link Property#JCR_LAST_MODIFIED_BY}. + */ + public final static String JCR_LAST_MODIFIED_BY = "jcr:lastModifiedBy"; + + /** + * @see Node#isNodeType(String) + * @throws JcrException caused by {@link RepositoryException} + */ + public static boolean isNodeType(Node node, String nodeTypeName) { + try { + return node.isNodeType(nodeTypeName); + } catch (RepositoryException e) { + throw new JcrException("Cannot get whether " + node + " is of type " + nodeTypeName, e); + } + } + + /** + * @see Node#hasNodes() + * @throws JcrException caused by {@link RepositoryException} + */ + public static boolean hasNodes(Node node) { + try { + return node.hasNodes(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get whether " + node + " has children.", e); + } + } + + /** + * @see Node#getParent() + * @throws JcrException caused by {@link RepositoryException} + */ + public static Node getParent(Node node) { + try { + return isRoot(node) ? null : node.getParent(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get parent of " + node, e); + } + } + + /** + * @see Node#getParent() + * @throws JcrException caused by {@link RepositoryException} + */ + public static String getParentPath(Node node) { + return getPath(getParent(node)); + } + + /** + * Whether this node is the root node. + * + * @throws JcrException caused by {@link RepositoryException} + */ + public static boolean isRoot(Node node) { + try { + return node.getDepth() == 0; + } catch (RepositoryException e) { + throw new JcrException("Cannot get depth of " + node, e); + } + } + + /** + * @see Node#getPath() + * @throws JcrException caused by {@link RepositoryException} + */ + public static String getPath(Node node) { + try { + return node.getPath(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get path of " + node, e); + } + } + + /** + * @see Node#getSession() + * @see Session#getWorkspace() + * @see Workspace#getName() + */ + public static String getWorkspaceName(Node node) { + return session(node).getWorkspace().getName(); + } + + /** + * @see Node#getIdentifier() + * @throws JcrException caused by {@link RepositoryException} + */ + public static String getIdentifier(Node node) { + try { + return node.getIdentifier(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get identifier of " + node, e); + } + } + + /** + * @see Node#getName() + * @throws JcrException caused by {@link RepositoryException} + */ + public static String getName(Node node) { + try { + return node.getName(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get name of " + node, e); + } + } + + /** + * Returns the node name with its current index (useful for re-ordering). + * + * @see Node#getName() + * @see Node#getIndex() + * @throws JcrException caused by {@link RepositoryException} + */ + public static String getIndexedName(Node node) { + try { + return node.getName() + "[" + node.getIndex() + "]"; + } catch (RepositoryException e) { + throw new JcrException("Cannot get name of " + node, e); + } + } + + /** + * @see Node#getProperty(String) + * @throws JcrException caused by {@link RepositoryException} + */ + public static Property getProperty(Node node, String property) { + try { + if (node.hasProperty(property)) + return node.getProperty(property); + else + return null; + } catch (RepositoryException e) { + throw new JcrException("Cannot get property " + property + " of " + node, e); + } + } + + /** + * @see Node#getIndex() + * @throws JcrException caused by {@link RepositoryException} + */ + public static int getIndex(Node node) { + try { + return node.getIndex(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get index of " + node, e); + } + } + + /** + * If node has mixin {@link NodeType#MIX_TITLE}, return + * {@link Property#JCR_TITLE}, otherwise return {@link #getName(Node)}. + */ + public static String getTitle(Node node) { + if (Jcr.isNodeType(node, NodeType.MIX_TITLE)) + return get(node, Property.JCR_TITLE); + else + return Jcr.getName(node); + } + + /** Accesses a {@link NodeIterator} as an {@link Iterable}. */ + @SuppressWarnings("unchecked") + public static Iterable iterate(NodeIterator nodeIterator) { + return new Iterable() { + + @Override + public Iterator iterator() { + return nodeIterator; + } + }; + } + + /** + * @return the children as an {@link Iterable} for use in for-each llops. + * @see Node#getNodes() + * @throws JcrException caused by {@link RepositoryException} + */ + public static Iterable nodes(Node node) { + try { + return iterate(node.getNodes()); + } catch (RepositoryException e) { + throw new JcrException("Cannot get children of " + node, e); + } + } + + /** + * @return the children as a (possibly empty) {@link List}. + * @see Node#getNodes() + * @throws JcrException caused by {@link RepositoryException} + */ + public static List getNodes(Node node) { + List nodes = new ArrayList<>(); + try { + if (node.hasNodes()) { + NodeIterator nit = node.getNodes(); + while (nit.hasNext()) + nodes.add(nit.nextNode()); + return nodes; + } else + return nodes; + } catch (RepositoryException e) { + throw new JcrException("Cannot get children of " + node, e); + } + } + + /** + * @return the child or null if not found + * @see Node#getNode(String) + * @throws JcrException caused by {@link RepositoryException} + */ + public static Node getNode(Node node, String child) { + try { + if (node.hasNode(child)) + return node.getNode(child); + else + return null; + } catch (RepositoryException e) { + throw new JcrException("Cannot get child of " + node, e); + } + } + + /** + * @return the node at this path or null if not found + * @see Session#getNode(String) + * @throws JcrException caused by {@link RepositoryException} + */ + public static Node getNode(Session session, String path) { + try { + if (session.nodeExists(path)) + return session.getNode(path); + else + return null; + } catch (RepositoryException e) { + throw new JcrException("Cannot get node " + path, e); + } + } + + /** + * Add a node to this parent, setting its primary type and its mixins. + * + * @param parent the parent node + * @param name the name of the node, if null, the primary + * type will be used (typically for XML structures) + * @param primaryType the primary type, if null + * {@link NodeType#NT_UNSTRUCTURED} will be used. + * @param mixins the mixins + * @return the created node + * @see Node#addNode(String, String) + * @see Node#addMixin(String) + */ + public static Node addNode(Node parent, String name, String primaryType, String... mixins) { + if (name == null && primaryType == null) + throw new IllegalArgumentException("Both node name and primary type cannot be null"); + try { + Node newNode = parent.addNode(name == null ? primaryType : name, + primaryType == null ? NodeType.NT_UNSTRUCTURED : primaryType); + for (String mixin : mixins) { + newNode.addMixin(mixin); + } + return newNode; + } catch (RepositoryException e) { + throw new JcrException("Cannot add node " + name + " to " + parent, e); + } + } + + /** + * Add an {@link NodeType#NT_BASE} node to this parent. + * + * @param parent the parent node + * @param name the name of the node, cannot be null + * @return the created node + * + * @see Node#addNode(String) + */ + public static Node addNode(Node parent, String name) { + if (name == null) + throw new IllegalArgumentException("Node name cannot be null"); + try { + Node newNode = parent.addNode(name); + return newNode; + } catch (RepositoryException e) { + throw new JcrException("Cannot add node " + name + " to " + parent, e); + } + } + + /** + * Add mixins to a node. + * + * @param node the node + * @param mixins the mixins + * @see Node#addMixin(String) + */ + public static void addMixin(Node node, String... mixins) { + try { + for (String mixin : mixins) { + node.addMixin(mixin); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot add mixins " + Arrays.asList(mixins) + " to " + node, e); + } + } + + /** + * Removes this node. + * + * @see Node#remove() + */ + public static void remove(Node node) { + try { + node.remove(); + } catch (RepositoryException e) { + throw new JcrException("Cannot remove node " + node, e); + } + } + + /** + * @return the node with htis id or null if not found + * @see Session#getNodeByIdentifier(String) + * @throws JcrException caused by {@link RepositoryException} + */ + public static Node getNodeById(Session session, String id) { + try { + return session.getNodeByIdentifier(id); + } catch (ItemNotFoundException e) { + return null; + } catch (RepositoryException e) { + throw new JcrException("Cannot get node with id " + id, e); + } + } + + /** + * Set a property to the given value, or remove it if the value is + * null. + * + * @throws JcrException caused by {@link RepositoryException} + */ + public static void set(Node node, String property, Object value) { + try { + if (!node.hasProperty(property)) { + if (value != null) { + if (value instanceof List) {// multiple + List lst = (List) value; + String[] values = new String[lst.size()]; + for (int i = 0; i < lst.size(); i++) { + values[i] = lst.get(i).toString(); + } + node.setProperty(property, values); + } else { + node.setProperty(property, value.toString()); + } + } + return; + } + Property prop = node.getProperty(property); + if (value == null) { + prop.remove(); + return; + } + + // multiple + if (value instanceof List) { + List lst = (List) value; + String[] values = new String[lst.size()]; + // TODO better cast? + for (int i = 0; i < lst.size(); i++) { + values[i] = lst.get(i).toString(); + } + if (!prop.isMultiple()) + prop.remove(); + node.setProperty(property, values); + return; + } + + // single + if (prop.isMultiple()) { + prop.remove(); + node.setProperty(property, value.toString()); + return; + } + + if (value instanceof String) + prop.setValue((String) value); + else if (value instanceof Long) + prop.setValue((Long) value); + else if (value instanceof Integer) + prop.setValue(((Integer) value).longValue()); + else if (value instanceof Double) + prop.setValue((Double) value); + else if (value instanceof Float) + prop.setValue(((Float) value).doubleValue()); + else if (value instanceof Calendar) + prop.setValue((Calendar) value); + else if (value instanceof BigDecimal) + prop.setValue((BigDecimal) value); + else if (value instanceof Boolean) + prop.setValue((Boolean) value); + else if (value instanceof byte[]) + JcrUtils.setBinaryAsBytes(prop, (byte[]) value); + else if (value instanceof Instant) { + Instant instant = (Instant) value; + GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTime(Date.from(instant)); + prop.setValue(calendar); + } else // try with toString() + prop.setValue(value.toString()); + } catch (RepositoryException e) { + throw new JcrException("Cannot set property " + property + " of " + node + " to " + value, e); + } + } + + /** + * Get property as {@link String}. + * + * @return the value of + * {@link Node#getProperty(String)}.{@link Property#getString()} or + * null if the property does not exist. + * @throws JcrException caused by {@link RepositoryException} + */ + public static String get(Node node, String property) { + return get(node, property, null); + } + + /** + * Get property as a {@link String}. If the property is multiple it returns the + * first value. + * + * @return the value of + * {@link Node#getProperty(String)}.{@link Property#getString()} or + * defaultValue if the property does not exist. + * @throws JcrException caused by {@link RepositoryException} + */ + public static String get(Node node, String property, String defaultValue) { + try { + if (node.hasProperty(property)) { + Property p = node.getProperty(property); + if (!p.isMultiple()) + return p.getString(); + else { + Value[] values = p.getValues(); + if (values.length == 0) + return defaultValue; + else + return values[0].getString(); + } + } else + return defaultValue; + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve property " + property + " from " + node, e); + } + } + + /** + * Get property as a {@link Value}. + * + * @return {@link Node#getProperty(String)} or null if the property + * does not exist. + * @throws JcrException caused by {@link RepositoryException} + */ + public static Value getValue(Node node, String property) { + try { + if (node.hasProperty(property)) + return node.getProperty(property).getValue(); + else + return null; + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve property " + property + " from " + node, e); + } + } + + /** + * Get property doing a best effort to cast it as the target object. + * + * @return the value of {@link Node#getProperty(String)} or + * defaultValue if the property does not exist. + * @throws IllegalArgumentException if the value could not be cast + * @throws JcrException in case of unexpected + * {@link RepositoryException} + */ + @SuppressWarnings("unchecked") + public static T getAs(Node node, String property, T defaultValue) { + try { + // TODO deal with multiple + if (node.hasProperty(property)) { + Property p = node.getProperty(property); + try { + if (p.isMultiple()) { + throw new UnsupportedOperationException("Multiple values properties are not supported"); + } + Value value = p.getValue(); + return (T) get(value); + } catch (ClassCastException e) { + throw new IllegalArgumentException( + "Cannot cast property of type " + PropertyType.nameFromValue(p.getType()), e); + } + } else { + return defaultValue; + } + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve property " + property + " from " + node, e); + } + } + + public static T getAs(Node node, String property, Class clss) { + if (String.class.isAssignableFrom(clss)) { + return (T) get(node, property); + } else if (Long.class.isAssignableFrom(clss)) { + return (T) get(node, property); + } else { + throw new IllegalArgumentException("Unsupported format " + clss); + } + } + + /** + * Get a multiple property as a list, doing a best effort to cast it as the + * target list. + * + * @return the value of {@link Node#getProperty(String)}. + * @throws IllegalArgumentException if the value could not be cast + * @throws JcrException in case of unexpected + * {@link RepositoryException} + */ + public static List getMultiple(Node node, String property) { + try { + if (node.hasProperty(property)) { + Property p = node.getProperty(property); + return getMultiple(p); + } else { + return null; + } + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve multiple values property " + property + " from " + node, e); + } + } + + /** + * Get a multiple property as a list, doing a best effort to cast it as the + * target list. + */ + @SuppressWarnings("unchecked") + public static List getMultiple(Property p) { + try { + List res = new ArrayList<>(); + if (!p.isMultiple()) { + res.add((T) get(p.getValue())); + return res; + } + Value[] values = p.getValues(); + for (Value value : values) { + res.add((T) get(value)); + } + return res; + } catch (ClassCastException | RepositoryException e) { + throw new IllegalArgumentException("Cannot get property " + p, e); + } + } + + /** Cast a {@link Value} to a standard Java object. */ + public static Object get(Value value) { + Binary binary = null; + try { + switch (value.getType()) { + case PropertyType.STRING: + return value.getString(); + case PropertyType.DOUBLE: + return (Double) value.getDouble(); + case PropertyType.LONG: + return (Long) value.getLong(); + case PropertyType.BOOLEAN: + return (Boolean) value.getBoolean(); + case PropertyType.DATE: + return value.getDate(); + case PropertyType.BINARY: + binary = value.getBinary(); + byte[] arr = null; + try (InputStream in = binary.getStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();) { + IOUtils.copy(in, out); + arr = out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("Cannot read binary from " + value, e); + } + return arr; + default: + return value.getString(); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot cast value from " + value, e); + } finally { + if (binary != null) + binary.dispose(); + } + } + + /** + * Retrieves the {@link Session} related to this node. + * + * @deprecated Use {@link #getSession(Node)} instead. + */ + @Deprecated + public static Session session(Node node) { + return getSession(node); + } + + /** Retrieves the {@link Session} related to this node. */ + public static Session getSession(Node node) { + try { + return node.getSession(); + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve session related to " + node, e); + } + } + + /** Retrieves the root node related to this session. */ + public static Node getRootNode(Session session) { + try { + return session.getRootNode(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get root node for " + session, e); + } + } + + /** Whether this item exists. */ + public static boolean itemExists(Session session, String path) { + try { + return session.itemExists(path); + } catch (RepositoryException e) { + throw new JcrException("Cannot check whether " + path + " exists", e); + } + } + + /** + * Saves the {@link Session} related to this node. Note that all other unrelated + * modifications in this session will also be saved. + */ + public static void save(Node node) { + try { + Session session = node.getSession(); +// if (node.isNodeType(NodeType.MIX_LAST_MODIFIED)) { +// set(node, Property.JCR_LAST_MODIFIED, Instant.now()); +// set(node, Property.JCR_LAST_MODIFIED_BY, session.getUserID()); +// } + if (session.hasPendingChanges()) + session.save(); + } catch (RepositoryException e) { + throw new JcrException("Cannot save session related to " + node + " in workspace " + + session(node).getWorkspace().getName(), e); + } + } + + /** Login to a JCR repository. */ + public static Session login(Repository repository, String workspace) { + try { + return repository.login(workspace); + } catch (RepositoryException e) { + throw new IllegalArgumentException("Cannot login to repository", e); + } + } + + /** Safely and silently logs out a session. */ + public static void logout(Session session) { + try { + if (session != null) + if (session.isLive()) + session.logout(); + } catch (Exception e) { + // silent + } + } + + /** Safely and silently logs out the underlying session. */ + public static void logout(Node node) { + Jcr.logout(session(node)); + } + + /* + * SECURITY + */ + /** + * Add a single privilege to a node. + * + * @see Privilege + */ + public static void addPrivilege(Node node, String principal, String privilege) { + try { + Session session = node.getSession(); + JcrUtils.addPrivilege(session, node.getPath(), principal, privilege); + } catch (RepositoryException e) { + throw new JcrException("Cannot add privilege " + privilege + " to " + node, e); + } + } + + /* + * VERSIONING + */ + /** Get checked out status. */ + public static boolean isCheckedOut(Node node) { + try { + return node.isCheckedOut(); + } catch (RepositoryException e) { + throw new JcrException("Cannot retrieve checked out status of " + node, e); + } + } + + /** @see VersionManager#checkpoint(String) */ + public static void checkpoint(Node node) { + try { + versionManager(node).checkpoint(node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot check in " + node, e); + } + } + + /** @see VersionManager#checkin(String) */ + public static void checkin(Node node) { + try { + versionManager(node).checkin(node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot check in " + node, e); + } + } + + /** @see VersionManager#checkout(String) */ + public static void checkout(Node node) { + try { + versionManager(node).checkout(node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot check out " + node, e); + } + } + + /** Get the {@link VersionManager} related to this node. */ + public static VersionManager versionManager(Node node) { + try { + return node.getSession().getWorkspace().getVersionManager(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get version manager from " + node, e); + } + } + + /** Get the {@link VersionHistory} related to this node. */ + public static VersionHistory getVersionHistory(Node node) { + try { + return versionManager(node).getVersionHistory(node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot get version history from " + node, e); + } + } + + /** + * The linear versions of this version history in reverse order and without the + * root version. + */ + public static List getLinearVersions(VersionHistory versionHistory) { + try { + List lst = new ArrayList<>(); + VersionIterator vit = versionHistory.getAllLinearVersions(); + while (vit.hasNext()) + lst.add(vit.nextVersion()); + lst.remove(0); + Collections.reverse(lst); + return lst; + } catch (RepositoryException e) { + throw new JcrException("Cannot get linear versions from " + versionHistory, e); + } + } + + /** The frozen node related to this {@link Version}. */ + public static Node getFrozenNode(Version version) { + try { + return version.getFrozenNode(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get frozen node from " + version, e); + } + } + + /** Get the base {@link Version} related to this node. */ + public static Version getBaseVersion(Node node) { + try { + return versionManager(node).getBaseVersion(node.getPath()); + } catch (RepositoryException e) { + throw new JcrException("Cannot get base version from " + node, e); + } + } + + /* + * FILES + */ + /** + * Returns the size of this file. + * + * @see NodeType#NT_FILE + */ + public static long getFileSize(Node fileNode) { + try { + if (!fileNode.isNodeType(NodeType.NT_FILE)) + throw new IllegalArgumentException(fileNode + " must be a file."); + return getBinarySize(fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary()); + } catch (RepositoryException e) { + throw new JcrException("Cannot get file size of " + fileNode, e); + } + } + + /** Returns the size of this {@link Binary}. */ + public static long getBinarySize(Binary binaryArg) { + try { + try (Bin binary = new Bin(binaryArg)) { + return binary.getSize(); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot get file size of binary " + binaryArg, e); + } + } + + // QUERY + /** Creates a JCR-SQL2 query using {@link MessageFormat}. */ + public static Query createQuery(QueryManager qm, String sql, Object... args) { + // fix single quotes + sql = sql.replaceAll("'", "''"); + String query = MessageFormat.format(sql, args); + try { + return qm.createQuery(query, Query.JCR_SQL2); + } catch (RepositoryException e) { + throw new JcrException("Cannot create JCR-SQL2 query from " + query, e); + } + } + + /** Executes a JCR-SQL2 query using {@link MessageFormat}. */ + public static NodeIterator executeQuery(QueryManager qm, String sql, Object... args) { + Query query = createQuery(qm, sql, args); + try { + return query.execute().getNodes(); + } catch (RepositoryException e) { + throw new JcrException("Cannot execute query " + sql + " with arguments " + Arrays.asList(args), e); + } + } + + /** Executes a JCR-SQL2 query using {@link MessageFormat}. */ + public static NodeIterator executeQuery(Session session, String sql, Object... args) { + QueryManager queryManager; + try { + queryManager = session.getWorkspace().getQueryManager(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get query manager from session " + session, e); + } + return executeQuery(queryManager, sql, args); + } + + /** + * Executes a JCR-SQL2 query using {@link MessageFormat}, which must return a + * single node at most. + * + * @return the node or null if not found. + */ + public static Node getNode(QueryManager qm, String sql, Object... args) { + NodeIterator nit = executeQuery(qm, sql, args); + if (nit.hasNext()) { + Node node = nit.nextNode(); + if (nit.hasNext()) + throw new IllegalStateException( + "Query " + sql + " with arguments " + Arrays.asList(args) + " returned more than one node."); + return node; + } else { + return null; + } + } + + /** + * Executes a JCR-SQL2 query using {@link MessageFormat}, which must return a + * single node at most. + * + * @return the node or null if not found. + */ + public static Node getNode(Session session, String sql, Object... args) { + QueryManager queryManager; + try { + queryManager = session.getWorkspace().getQueryManager(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get query manager from session " + session, e); + } + return getNode(queryManager, sql, args); + } + + public static Node getRowNode(Row row, String selectorName) { + try { + return row.getNode(selectorName); + } catch (RepositoryException e) { + throw new JcrException("Cannot get node " + selectorName + " from row", e); + } + } + + /** Singleton. */ + private Jcr() { + + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrAuthorizations.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrAuthorizations.java new file mode 100644 index 0000000..351929f --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrAuthorizations.java @@ -0,0 +1,207 @@ +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
+ * value := group1,group2,user1 + */ + private Map principalPrivileges = new HashMap(); + + 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 (RepositoryException e) { + JcrUtils.discardQuietly(session); + throw new JcrException( + "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 (RepositoryException e) { + JcrUtils.discardQuietly(session); + throw new JcrException( + "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 IllegalArgumentException("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 privs = new ArrayList(); + 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 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 groupPrivileges) { + this.principalPrivileges = groupPrivileges; + } + + public void setPrincipalPrivileges(Map 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.cms.jcr/src/org/argeo/jcr/JcrCallback.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrCallback.java new file mode 100644 index 0000000..efbaabe --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrCallback.java @@ -0,0 +1,15 @@ +package org.argeo.jcr; + +import java.util.function.Function; + +import javax.jcr.Session; + +/** An arbitrary execution on a JCR session, optionally returning a result. */ +@FunctionalInterface +public interface JcrCallback extends Function { + /** @deprecated Use {@link #apply(Session)} instead. */ + @Deprecated + public default Object execute(Session session) { + return apply(session); + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrException.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrException.java new file mode 100644 index 0000000..c778743 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrException.java @@ -0,0 +1,22 @@ +package org.argeo.jcr; + +import javax.jcr.RepositoryException; + +/** + * Wraps a {@link RepositoryException} in a {@link RuntimeException}. + */ +public class JcrException extends IllegalStateException { + private static final long serialVersionUID = -4530350094877964989L; + + public JcrException(String message, RepositoryException e) { + super(message, e); + } + + public JcrException(RepositoryException e) { + super(e); + } + + public RepositoryException getRepositoryCause() { + return (RepositoryException) getCause(); + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrMonitor.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrMonitor.java new file mode 100644 index 0000000..71cf961 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrMonitor.java @@ -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 UNKNOWN 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 true if cancellation has been requested, and + * false otherwise + * @see #setCanceled(boolean) + */ + public boolean isCanceled(); + + /** + * Sets the cancel state to the given value. + * + * @param value + * true indicates that cancelation has been + * requested (but not necessarily acknowledged); + * false 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.cms.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java new file mode 100644 index 0000000..3228eee --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrRepositoryWrapper.java @@ -0,0 +1,244 @@ +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 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 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(workspaceName).login(credentials, workspaceName); + } catch (NoSuchWorkspaceException e) { + if (autocreateWorkspaces && workspaceName != null) + session = createWorkspaceAndLogsIn(credentials, workspaceName); + else + throw e; + } + processNewSession(session, workspaceName); + 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, String workspaceName) { + } + + /** + * Wraps access to the repository, making sure it is available. + * + * @deprecated Use {@link #getDefaultRepository()} instead. + */ + @Deprecated + protected synchronized Repository getRepository() { + return getDefaultRepository(); + } + + protected synchronized Repository getDefaultRepository() { + return repository; + } + + protected synchronized Repository getRepository(String workspaceName) { + return getDefaultRepository(); + } + + /** + * 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 IllegalArgumentException("No workspace specified."); + Session session = getRepository(workspaceName).login(credentials); + session.getWorkspace().createWorkspace(workspaceName); + session.logout(); + return getRepository(workspaceName).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.cms.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java new file mode 100644 index 0000000..82a65e7 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrUrlStreamHandler.java @@ -0,0 +1,70 @@ +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.cms.jcr/src/org/argeo/jcr/JcrUtils.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrUtils.java new file mode 100644 index 0000000..1f1fa11 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrUtils.java @@ -0,0 +1,1786 @@ +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.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.text.DateFormat; +import java.text.ParseException; +import java.time.Instant; +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.Credentials; +import javax.jcr.ImportUUIDBehavior; +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.NoSuchNodeTypeException; +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; + +/** 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 JcrException 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 JcrException("Cannot execute query " + query, e); + } + Node node; + if (nodeIterator.hasNext()) + node = nodeIterator.nextNode(); + else + return null; + + if (nodeIterator.hasNext()) + throw new IllegalArgumentException("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 IllegalArgumentException("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 IllegalArgumentException("Root path '/' has no parent path"); + if (path.charAt(0) != '/') + throw new IllegalArgumentException("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 IllegalArgumentException("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 (RepositoryException e) { + throw new JcrException("Cannot set URL " + url + " as nt:address properties", e); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("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 (RepositoryException e) { + throw new JcrException("Cannot get URL from nt:address properties of " + node, e); + } catch (MalformedURLException e) { + throw new IllegalArgumentException("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 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 IllegalArgumentException("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 IllegalArgumentException("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 JcrException("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 JcrException("Cannot get name from " + node, e); + } + } + +// /** +// * Routine that get the child with this name, adding it if it does not already +// * exist +// */ +// public static Node getOrAdd(Node parent, String name, String primaryNodeType) throws RepositoryException { +// return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name, primaryNodeType); +// } + + /** + * Routine that get the child with this name, adding it if it does not already + * exist + */ + public static Node getOrAdd(Node parent, String name, String primaryNodeType, String... mixinNodeTypes) + throws RepositoryException { + Node node; + if (parent.hasNode(name)) { + node = parent.getNode(name); + if (primaryNodeType != null && !node.isNodeType(primaryNodeType)) + throw new IllegalArgumentException("Node " + node + " exists but is of primary node type " + + node.getPrimaryNodeType().getName() + ", not " + primaryNodeType); + for (String mixin : mixinNodeTypes) { + if (!node.isNodeType(mixin)) + node.addMixin(mixin); + } + return node; + } else { + node = primaryNodeType != null ? parent.addNode(name, primaryNodeType) : parent.addNode(name); + for (String mixin : mixinNodeTypes) { + node.addMixin(mixin); + } + return node; + } + } + + /** + * Routine that get the child with this name, adding it if it does not already + * exist + */ + public static Node getOrAdd(Node parent, String name) throws RepositoryException { + return parent.hasNode(name) ? parent.getNode(name) : parent.addNode(name); + } + + /** Convert a {@link NodeIterator} to a list of {@link Node} */ + public static List nodeIteratorToList(NodeIterator nodeIterator) { + List nodes = new ArrayList(); + 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 JcrException("Cannot get property " + propertyName + " of " + node, e); + } + } + + /** Concisely get the path of the given node. */ + public static String getPath(Node node) { + try { + return node.getPath(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get path 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 JcrException("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 JcrException("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 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 JcrException("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 IllegalStateException("Session has pending changes, save them first."); + Node node = mkdirs(session, path, type); + session.save(); + return node; + } catch (RepositoryException e) { + discardQuietly(session); + throw new JcrException("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 IllegalArgumentException("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 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 JcrException("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 tokenize(String path) { + List tokens = new ArrayList(); + 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 JcrException("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 IllegalArgumentException("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 JcrException("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 JcrException("Cannot write summary of " + acl, e); + } + } + + /** Copy the whole workspace via a system view XML. */ + public static void copyWorkspaceXml(Session fromSession, Session toSession) { + Workspace fromWorkspace = fromSession.getWorkspace(); + Workspace toWorkspace = toSession.getWorkspace(); + String errorMsg = "Cannot copy workspace " + fromWorkspace + " to " + toWorkspace + " via XML."; + + try (PipedInputStream in = new PipedInputStream(1024 * 1024);) { + new Thread(() -> { + try (PipedOutputStream out = new PipedOutputStream(in)) { + fromSession.exportSystemView("/", out, false, false); + out.flush(); + } catch (IOException e) { + throw new RuntimeException(errorMsg, e); + } catch (RepositoryException e) { + throw new JcrException(errorMsg, e); + } + }, "Copy workspace" + fromWorkspace + " to " + toWorkspace).start(); + + toSession.importXML("/", in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING); + toSession.save(); + } catch (IOException e) { + throw new RuntimeException(errorMsg, e); + } catch (RepositoryException e) { + throw new JcrException(errorMsg, 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; + + // add mixins + for (NodeType mixinType : fromNode.getMixinNodeTypes()) { + try { + toNode.addMixin(mixinType.getName()); + } catch (NoSuchNodeTypeException e) { + // ignore unknown mixins + // TODO log it + } + } + + // 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, true); + + // 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 { + try { + toChild = toNode.addNode(fromChild.getName(), fromChild.getPrimaryNodeType().getName()); + } catch (NoSuchNodeTypeException e) { + // ignore unknown primary types + // TODO log it + return; + } + } + copy(fromChild, toChild); + } + } catch (RepositoryException e) { + throw new JcrException("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 JcrException("Cannot check all properties equals of " + reference + " and " + observed, e); + } + } + + public static Map diffProperties(Node reference, Node observed) { + Map diffs = new TreeMap(); + 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 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 JcrException("Cannot diff " + reference + " and " + observed, e); + } + } + + /** + * Compare only a restricted list of properties of two nodes. No recursivity. + * + */ + public static Map diffProperties(Node reference, Node observed, List properties) { + Map diffs = new TreeMap(); + try { + Iterator 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 JcrException("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) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + Bin binary = new Bin(property); + InputStream in = binary.getStream()) { + IOUtils.copy(in, out); + return out.toByteArray(); + } catch (RepositoryException e) { + throw new JcrException("Cannot read binary " + property + " as bytes", e); + } catch (IOException e) { + throw new RuntimeException("Cannot read binary " + property + " as bytes", e); + } + } + + /** Writes a {@link Binary} from a byte array */ + public static void setBinaryAsBytes(Node node, String property, byte[] bytes) { + Binary binary = null; + try (InputStream in = new ByteArrayInputStream(bytes)) { + binary = node.getSession().getValueFactory().createBinary(in); + node.setProperty(property, binary); + } catch (RepositoryException e) { + throw new JcrException("Cannot set binary " + property + " as bytes", e); + } catch (IOException e) { + throw new RuntimeException("Cannot set binary " + property + " as bytes", e); + } finally { + closeQuietly(binary); + } + } + + /** Writes a {@link Binary} from a byte array */ + public static void setBinaryAsBytes(Property prop, byte[] bytes) { + Binary binary = null; + try (InputStream in = new ByteArrayInputStream(bytes)) { + binary = prop.getSession().getValueFactory().createBinary(in); + prop.setValue(binary); + } catch (RepositoryException e) { + throw new JcrException("Cannot set binary " + prop + " as bytes", e); + } catch (IOException e) { + throw new RuntimeException("Cannot set binary " + prop + " as bytes", e); + } finally { + 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 IllegalArgumentException("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) { + // silent + } + } + + /** + * Discards the current changes in a session by calling + * {@link Session#refresh(boolean)} with false, 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) { + // silent + } + } + + /** + * 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 { + return loginOrCreateWorkspace(repository, workspaceName, null); + } + + /** + * 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, Credentials credentials) + throws RepositoryException { + Session workspaceSession = null; + Session defaultSession = null; + try { + try { + workspaceSession = repository.login(credentials, workspaceName); + } catch (NoSuchWorkspaceException e) { + // try to create workspace + defaultSession = repository.login(credentials); + defaultSession.getWorkspace().createWorkspace(workspaceName); + + // work around non-atomicity of workspace creation in Jackrabbit +// try { +// Thread.sleep(5000); +// } catch (InterruptedException e1) { +// // ignore +// } + + workspaceSession = repository.login(credentials, workspaceName); + } + return workspaceSession; + } finally { + logoutQuietly(defaultSession); + } + } + + /** + * Logs out the session, not throwing any exception, even if it is null. + * {@link Jcr#logout(Session)} should rather be used. + */ + public static void logoutQuietly(Session session) { + Jcr.logout(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 JcrException("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 + } + } + + /** 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 + } + } + + /** + * Checks whether {@link Property#JCR_LAST_MODIFIED} or (afterwards) + * {@link Property#JCR_CREATED} are set and returns it as an {@link Instant}. + */ + public static Instant getModified(Node node) { + Calendar calendar = null; + try { + if (node.hasProperty(Property.JCR_LAST_MODIFIED)) + calendar = node.getProperty(Property.JCR_LAST_MODIFIED).getDate(); + else if (node.hasProperty(Property.JCR_CREATED)) + calendar = node.getProperty(Property.JCR_CREATED).getDate(); + else + throw new IllegalArgumentException("No modification time found in " + node); + return calendar.toInstant(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get modification time for " + node, e); + } + + } + + /** + * Get {@link Property#JCR_CREATED} as an {@link Instant}, if it is set. + */ + public static Instant getCreated(Node node) { + Calendar calendar = null; + try { + if (node.hasProperty(Property.JCR_CREATED)) + calendar = node.getProperty(Property.JCR_CREATED).getDate(); + else + throw new IllegalArgumentException("No created time found in " + node); + return calendar.toInstant(); + } catch (RepositoryException e) { + throw new JcrException("Cannot get created time for " + node, e); + } + + } + + /** + * 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. + */ + public static void updateLastModified(Node node) { + updateLastModified(node, false); + } + + /** + * 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, + * these properties are + * not automatically updated, hence the need for manual update. The session + * is not saved. + */ + public static void updateLastModified(Node node, boolean addMixin) { + try { + if (addMixin && !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 JcrException("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) { + updateLastModifiedAndParents(node, untilPath, true); + } + + /** + * 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, boolean addMixin) { + try { + if (untilPath != null && !node.getPath().startsWith(untilPath)) + throw new IllegalArgumentException(node + " is not under " + untilPath); + updateLastModified(node, addMixin); + if (untilPath == null) { + if (!node.getPath().equals("/")) + updateLastModifiedAndParents(node.getParent(), untilPath, addMixin); + } else { + if (!node.getPath().equals(untilPath)) + updateLastModifiedAndParents(node.getParent(), untilPath, addMixin); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot update lastModified from " + node + " until " + untilPath, e); + } + } + + /** + * Returns a String representing the short version (see + * Node type + * Notation 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 JcrException("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 JcrException("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 privileges = new ArrayList(); + 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 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 the first available 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); + applicablePolicies: if (policyIterator.hasNext()) { + while (policyIterator.hasNext()) { + AccessControlPolicy acp = policyIterator.nextAccessControlPolicy(); + if (acp instanceof AccessControlList) { + acl = ((AccessControlList) acp); + break applicablePolicies; + } + } + } else { + AccessControlPolicy[] existingPolicies = acm.getPolicies(path); + existingPolicies: for (AccessControlPolicy acp : existingPolicies) { + if (acp instanceof AccessControlList) { + acl = ((AccessControlList) acp); + break existingPolicies; + } + } + } + if (acl != null) + return acl; + else + throw new IllegalArgumentException("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); + session.refresh(true); + session.save(); + } + + /* + * 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 IllegalStateException("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); + } catch (IOException e) { + throw new RuntimeException("Cannot copy " + fileName + " to " + toNode, e); + } + + // 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 IllegalArgumentException(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 e) { + throw new JcrException("Cannot copy files between " + fromNode + " and " + toNode, e); + } 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 JcrException("Cannot count all children of " + node, e); + } + return localCount; + } + + /** + * Copy a file as an nt:file, assuming an nt:folder hierarchy. The session is + * NOT saved. + * + * @return the created file node + */ + @Deprecated + public static Node copyFile(Node folderNode, File file) { + try (InputStream in = new FileInputStream(file)) { + return copyStreamAsFile(folderNode, file.getName(), in); + } catch (IOException e) { + throw new RuntimeException("Cannot copy file " + file + " under " + folderNode, e); + } + } + + /** 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 (IOException e) { + throw new RuntimeException("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 IllegalArgumentException(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_UNSTRUCTURED); + } + binary = contentNode.getSession().getValueFactory().createBinary(in); + contentNode.setProperty(Property.JCR_DATA, binary); + updateLastModified(contentNode); + return fileNode; + } catch (RepositoryException e) { + throw new JcrException("Cannot create file node " + fileName + " under " + folderNode, e); + } finally { + closeQuietly(binary); + } + } + + /** Read an an nt:file as an {@link InputStream}. */ + public static InputStream getFileAsStream(Node fileNode) throws RepositoryException { + return fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary().getStream(); + } + + /** + * Set the properties of {@link NodeType#MIX_MIMETYPE} on the content of this + * file node. + */ + public static void setFileMimeType(Node fileNode, String mimeType, String encoding) throws RepositoryException { + Node contentNode = fileNode.getNode(Node.JCR_CONTENT); + if (mimeType != null) + contentNode.setProperty(Property.JCR_MIMETYPE, mimeType); + if (encoding != null) + contentNode.setProperty(Property.JCR_ENCODING, encoding); + // TODO remove properties if args are null? + } + + public static void copyFilesToFs(Node baseNode, Path targetDir, boolean recursive) { + try { + Files.createDirectories(targetDir); + for (NodeIterator nit = baseNode.getNodes(); nit.hasNext();) { + Node node = nit.nextNode(); + if (node.isNodeType(NodeType.NT_FILE)) { + Path filePath = targetDir.resolve(node.getName()); + try (OutputStream out = Files.newOutputStream(filePath); InputStream in = getFileAsStream(node)) { + IOUtils.copy(in, out); + } + } else if (recursive && node.isNodeType(NodeType.NT_FOLDER)) { + Path dirPath = targetDir.resolve(node.getName()); + copyFilesToFs(node, dirPath, true); + } + } + } catch (RepositoryException e) { + throw new JcrException("Cannot copy " + baseNode + " to " + targetDir, e); + } catch (IOException e) { + throw new RuntimeException("Cannot copy " + baseNode + " to " + targetDir, e); + } + } + + /** + * Computes the checksum of an nt:file. + * + * @deprecated use separate digest utilities + */ + @Deprecated + public static String checksumFile(Node fileNode, String algorithm) { + try (InputStream in = fileNode.getNode(Node.JCR_CONTENT).getProperty(Property.JCR_DATA).getBinary() + .getStream()) { + return digest(algorithm, in); + } catch (IOException e) { + throw new RuntimeException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e); + } catch (RepositoryException e) { + throw new JcrException("Cannot checksum file " + fileNode + " with algorithm " + algorithm, e); + } + } + + @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 (IOException e) { + throw new RuntimeException("Cannot digest with algorithm " + algorithm, e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("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); + } + + /** Export a subtree as a compact XML without namespaces. */ + public static void toSimpleXml(Node node, StringBuilder sb) throws RepositoryException { + sb.append('<'); + String nodeName = node.getName(); + int colIndex = nodeName.indexOf(':'); + if (colIndex > 0) { + nodeName = nodeName.substring(colIndex + 1); + } + sb.append(nodeName); + PropertyIterator pit = node.getProperties(); + properties: while (pit.hasNext()) { + Property p = pit.nextProperty(); + // skip multiple properties + if (p.isMultiple()) + continue properties; + String propertyName = p.getName(); + int pcolIndex = propertyName.indexOf(':'); + // skip properties with namespaces + if (pcolIndex > 0) + continue properties; + // skip binaries + if (p.getType() == PropertyType.BINARY) { + continue properties; + // TODO retrieve identifier? + } + sb.append(' '); + sb.append(propertyName); + sb.append('='); + sb.append('\"').append(p.getString()).append('\"'); + } + + if (node.hasNodes()) { + sb.append('>'); + NodeIterator children = node.getNodes(); + while (children.hasNext()) { + toSimpleXml(children.nextNode(), sb); + } + sb.append("'); + } else { + sb.append("/>"); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxApi.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxApi.java new file mode 100644 index 0000000..666b259 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxApi.java @@ -0,0 +1,190 @@ +package org.argeo.jcr; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Value; + +/** Uilities around the JCR extensions. */ +public class JcrxApi { + public final static String MD5 = "MD5"; + public final static String SHA1 = "SHA1"; + public final static String SHA256 = "SHA-256"; + public final static String SHA512 = "SHA-512"; + + public final static String EMPTY_MD5 = "d41d8cd98f00b204e9800998ecf8427e"; + public final static String EMPTY_SHA1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + public final static String EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + public final static String EMPTY_SHA512 = "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"; + + public final static int LENGTH_MD5 = EMPTY_MD5.length(); + public final static int LENGTH_SHA1 = EMPTY_SHA1.length(); + public final static int LENGTH_SHA256 = EMPTY_SHA256.length(); + public final static int LENGTH_SHA512 = EMPTY_SHA512.length(); + + /* + * XML + */ + /** + * Get the XML text of this child node. + */ + public static String getXmlValue(Node node, String name) { + try { + if (!node.hasNode(name)) + return null; + Node child = node.getNode(name); + return getXmlValue(child); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get " + name + " as XML text", e); + } + } + + /** + * Get the XML text of this node. + */ + public static String getXmlValue(Node node) { + try { + if (!node.hasNode(Jcr.JCR_XMLTEXT)) + return null; + Node xmlText = node.getNode(Jcr.JCR_XMLTEXT); + if (!xmlText.hasProperty(Jcr.JCR_XMLCHARACTERS)) + throw new IllegalArgumentException( + "Node " + xmlText + " has no " + Jcr.JCR_XMLCHARACTERS + " property"); + return xmlText.getProperty(Jcr.JCR_XMLCHARACTERS).getString(); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get " + node + " as XML text", e); + } + } + + /** + * Set as a subnode which will be exported as an XML element. + */ + public static void setXmlValue(Node node, String name, String value) { + try { + if (node.hasNode(name)) { + Node child = node.getNode(name); + setXmlValue(node, child, value); + } else + node.addNode(name, JcrxType.JCRX_XMLVALUE).addNode(Jcr.JCR_XMLTEXT, JcrxType.JCRX_XMLTEXT) + .setProperty(Jcr.JCR_XMLCHARACTERS, value); + } catch (RepositoryException e) { + throw new JcrException("Cannot set " + name + " as XML text", e); + } + } + + public static void setXmlValue(Node node, Node child, String value) { + try { + if (!child.hasNode(Jcr.JCR_XMLTEXT)) + child.addNode(Jcr.JCR_XMLTEXT, JcrxType.JCRX_XMLTEXT); + child.getNode(Jcr.JCR_XMLTEXT).setProperty(Jcr.JCR_XMLCHARACTERS, value); + } catch (RepositoryException e) { + throw new JcrException("Cannot set " + child + " as XML text", e); + } + } + + /** + * Add a checksum replacing the one which was previously set with the same + * length. + */ + public static void addChecksum(Node node, String checksum) { + try { + if (!node.hasProperty(JcrxName.JCRX_SUM)) { + node.setProperty(JcrxName.JCRX_SUM, new String[] { checksum }); + return; + } else { + int stringLength = checksum.length(); + Property property = node.getProperty(JcrxName.JCRX_SUM); + List values = Arrays.asList(property.getValues()); + Integer indexToRemove = null; + values: for (int i = 0; i < values.size(); i++) { + Value value = values.get(i); + if (value.getString().length() == stringLength) { + indexToRemove = i; + break values; + } + } + if (indexToRemove != null) + values.set(indexToRemove, node.getSession().getValueFactory().createValue(checksum)); + else + values.add(0, node.getSession().getValueFactory().createValue(checksum)); + property.setValue(values.toArray(new Value[values.size()])); + } + } catch (RepositoryException e) { + throw new JcrException("Cannot set checksum on " + node, e); + } + } + + /** Replace all checksums. */ + public static void setChecksums(Node node, List checksums) { + try { + node.setProperty(JcrxName.JCRX_SUM, checksums.toArray(new String[checksums.size()])); + } catch (RepositoryException e) { + throw new JcrException("Cannot set checksums on " + node, e); + } + } + + /** Replace all checksums. */ + public static List getChecksums(Node node) { + try { + List res = new ArrayList<>(); + if (!node.hasProperty(JcrxName.JCRX_SUM)) + return res; + Property property = node.getProperty(JcrxName.JCRX_SUM); + for (Value value : property.getValues()) { + res.add(value.getString()); + } + return res; + } catch (RepositoryException e) { + throw new JcrException("Cannot get checksums from " + node, e); + } + } + +// /** Replace all checksums with this single one. */ +// public static void setChecksum(Node node, String checksum) { +// setChecksums(node, Collections.singletonList(checksum)); +// } + + /** Retrieves the checksum with this algorithm, or null if not found. */ + public static String getChecksum(Node node, String algorithm) { + int stringLength; + switch (algorithm) { + case MD5: + stringLength = LENGTH_MD5; + break; + case SHA1: + stringLength = LENGTH_SHA1; + break; + case SHA256: + stringLength = LENGTH_SHA256; + break; + case SHA512: + stringLength = LENGTH_SHA512; + break; + default: + throw new IllegalArgumentException("Unkown algorithm " + algorithm); + } + return getChecksum(node, stringLength); + } + + /** Retrieves the checksum with this string length, or null if not found. */ + public static String getChecksum(Node node, int stringLength) { + try { + if (!node.hasProperty(JcrxName.JCRX_SUM)) + return null; + Property property = node.getProperty(JcrxName.JCRX_SUM); + for (Value value : property.getValues()) { + String str = value.getString(); + if (str.length() == stringLength) + return str; + } + return null; + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot get checksum for " + node, e); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxName.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxName.java new file mode 100644 index 0000000..9dd43ad --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxName.java @@ -0,0 +1,7 @@ +package org.argeo.jcr; + +/** Names declared by the JCR extensions. */ +public interface JcrxName { + /** The multiple property holding various coherent checksums. */ + public final static String JCRX_SUM = "{http://www.argeo.org/ns/jcrx}sum"; +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxType.java b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxType.java new file mode 100644 index 0000000..0cbad33 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/JcrxType.java @@ -0,0 +1,17 @@ +package org.argeo.jcr; + +/** Node types declared by the JCR extensions. */ +public interface JcrxType { + /** + * Node type for an XML value, which will be serialized in XML as an element + * containing text. + */ + public final static String JCRX_XMLVALUE = "{http://www.argeo.org/ns/jcrx}xmlvalue"; + + /** Node type for the node containing the text. */ + public final static String JCRX_XMLTEXT = "{http://www.argeo.org/ns/jcrx}xmltext"; + + /** Mixin node type for a set of checksums. */ + public final static String JCRX_CSUM = "{http://www.argeo.org/ns/jcrx}csum"; + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/PropertyDiff.java b/org.argeo.cms.jcr/src/org/argeo/jcr/PropertyDiff.java new file mode 100644 index 0000000..71e76fe --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/PropertyDiff.java @@ -0,0 +1,57 @@ +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 IllegalArgumentException("Reference and new values must be specified."); + } else if (type == ADDED) { + if (referenceValue != null || newValue == null) + throw new IllegalArgumentException("New value and only it must be specified."); + } else if (type == REMOVED) { + if (referenceValue == null || newValue != null) + throw new IllegalArgumentException("Reference value and only it must be specified."); + } else { + throw new IllegalArgumentException("Unkown diff type " + type); + } + + if (relPath == null) + throw new IllegalArgumentException("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.cms.jcr/src/org/argeo/jcr/SimplePrincipal.java b/org.argeo.cms.jcr/src/org/argeo/jcr/SimplePrincipal.java new file mode 100644 index 0000000..4f42f2d --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/SimplePrincipal.java @@ -0,0 +1,43 @@ +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.cms.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java b/org.argeo.cms.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java new file mode 100644 index 0000000..2208627 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/ThreadBoundJcrSessionFactory.java @@ -0,0 +1,279 @@ +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.argeo.api.cms.CmsLog; + +/** Proxy JCR sessions and attach them to calling threads. */ +@Deprecated +public abstract class ThreadBoundJcrSessionFactory { + private final static CmsLog log = CmsLog.getLog(ThreadBoundJcrSessionFactory.class); + + private Repository repository; + /** can be injected as list, only used if repository is null */ + private List repositories; + + private ThreadLocal session = new ThreadLocal(); + 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 threads = Collections.synchronizedList(new ArrayList()); + private final Map activeSessions = Collections.synchronizedMap(new HashMap()); + 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 IllegalStateException("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 JcrException("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 JcrException("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 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 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 it = repositories.iterator(); + if (it.hasNext()) + return it.next(); + } + throw new IllegalStateException("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 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.cms.jcr/src/org/argeo/jcr/VersionDiff.java b/org.argeo.cms.jcr/src/org/argeo/jcr/VersionDiff.java new file mode 100644 index 0000000..dab5554 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/VersionDiff.java @@ -0,0 +1,38 @@ +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 diffs; + private Calendar updateTime; + + public VersionDiff(String userId, Calendar updateTime, + Map diffs) { + this.userId = userId; + this.updateTime = updateTime; + this.diffs = diffs; + } + + public String getUserId() { + return userId; + } + + public Map getDiffs() { + return diffs; + } + + public Calendar getUpdateTime() { + return updateTime; + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/BinaryChannel.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/BinaryChannel.java new file mode 100644 index 0000000..d6550fe --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/BinaryChannel.java @@ -0,0 +1,190 @@ +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.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; + +/** A read/write {@link SeekableByteChannel} based on a {@link Binary}. */ +public class BinaryChannel implements SeekableByteChannel { + private final Node file; + private Binary binary; + private boolean open = true; + + private long position = 0; + + private FileChannel fc = null; + + public BinaryChannel(Node file, Path path) throws RepositoryException, IOException { + this.file = file; + Session session = file.getSession(); + synchronized (session) { + if (file.isNodeType(NodeType.NT_FILE)) { + if (file.hasNode(Node.JCR_CONTENT)) { + Node data = file.getNode(Property.JCR_CONTENT); + this.binary = data.getProperty(Property.JCR_DATA).getBinary(); + } else { + Node data = file.addNode(Node.JCR_CONTENT, NodeType.NT_UNSTRUCTURED); + data.addMixin(NodeType.MIX_LAST_MODIFIED); + 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 = Files.probeContentType(path); + // String mime = fileTypeMap.getContentType(file.getName()); + data.setProperty(Property.JCR_MIMETYPE, mime); + + session.refresh(true); + session.save(); + session.notifyAll(); + } + } 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(); + synchronized (session) { + fc.position(0); + InputStream in = Channels.newInputStream(fc); + newBinary = session.getValueFactory().createBinary(in); + file.getNode(Property.JCR_CONTENT).setProperty(Property.JCR_DATA, newBinary); + session.refresh(true); + session.save(); + open = false; + session.notifyAll(); + } + } 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; + byte[] arr = dst.array(); + read = binary.read(arr, position); + + 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; + } + + @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); + 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.cms.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java new file mode 100644 index 0000000..7c9711b --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrBasicfileAttributes.java @@ -0,0 +1,138 @@ +package org.argeo.jcr.fs; + +import static javax.jcr.Property.JCR_CREATED; +import static javax.jcr.Property.JCR_LAST_MODIFIED; + +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 final static 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.hasProperty(JCR_LAST_MODIFIED)) { + Instant instant = node.getProperty(JCR_LAST_MODIFIED).getDate().toInstant(); + return FileTime.from(instant); + } else if (node.hasProperty(JCR_CREATED)) { + Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant(); + return FileTime.from(instant); + } +// 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.hasProperty(JCR_CREATED)) { + Instant instant = node.getProperty(JCR_CREATED).getDate().toInstant(); + return FileTime.from(instant); + } else if (node.hasProperty(JCR_LAST_MODIFIED)) { + Instant instant = node.getProperty(JCR_LAST_MODIFIED).getDate().toInstant(); + return FileTime.from(instant); + } +// if (node.isNodeType(NodeType.MIX_CREATED)) { +// Instant instant = node.getProperty(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() { + try { + return node.getIdentifier(); + } catch (RepositoryException e) { + throw new JcrFsException("Cannot get identifier", e); + } + } + + @Override + public Node getNode() { + return node; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java new file mode 100644 index 0000000..4b32981 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystem.java @@ -0,0 +1,252 @@ +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.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import javax.jcr.Credentials; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; + +import org.argeo.api.acr.fs.AbstractFsStore; +import org.argeo.api.acr.fs.AbstractFsSystem; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrUtils; + +public class JcrFileSystem extends AbstractFsSystem { + private final JcrFileSystemProvider provider; + + private final Repository repository; + private Session session; + private WorkspaceFileStore baseFileStore; + + private Map mounts = new TreeMap<>(); + + private String userHomePath = null; + + @Deprecated + public JcrFileSystem(JcrFileSystemProvider provider, Session session) throws IOException { + super(); + this.provider = provider; + baseFileStore = new WorkspaceFileStore(null, session.getWorkspace()); + 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); +// } + this.repository = null; + } + + public JcrFileSystem(JcrFileSystemProvider provider, Repository repository) throws IOException { + this(provider, repository, null); + } + + public JcrFileSystem(JcrFileSystemProvider provider, Repository repository, Credentials credentials) + throws IOException { + super(); + this.provider = provider; + this.repository = repository; + try { + this.session = credentials == null ? repository.login() : repository.login(credentials); + baseFileStore = new WorkspaceFileStore(null, session.getWorkspace()); + workspaces: for (String workspaceName : baseFileStore.getWorkspace().getAccessibleWorkspaceNames()) { + if (workspaceName.equals(baseFileStore.getWorkspace().getName())) + continue workspaces;// do not mount base + if (workspaceName.equals("security")) { + continue workspaces;// do not mount security workspace + // TODO make it configurable + } + Session mountSession = credentials == null ? repository.login(workspaceName) + : repository.login(credentials, workspaceName); + String mountPath = JcrPath.separator + workspaceName; + mounts.put(mountPath, new WorkspaceFileStore(mountPath, mountSession.getWorkspace())); + } + } catch (RepositoryException e) { + throw new IOException("Cannot initialise file system", e); + } + + Node userHome = provider.getUserHome(repository); + if (userHome != null) + try { + userHomePath = toFsPath(userHome); + } catch (RepositoryException e) { + throw new IOException("Cannot retrieve user home path", e); + } finally { + JcrUtils.logoutQuietly(Jcr.session(userHome)); + } + } + + public String toFsPath(Node node) throws RepositoryException { + return getFileStore(node).toFsPath(node); + } + + /** Whether this node should be skipped in directory listings */ + public boolean skipNode(Node node) throws RepositoryException { + if (node.isNodeType(NodeType.NT_HIERARCHY_NODE)) + return false; + return true; + } + + public String getUserHomePath() { + return userHomePath; + } + + public WorkspaceFileStore getFileStore(String path) { + WorkspaceFileStore res = baseFileStore; + for (String mountPath : mounts.keySet()) { + if (path.equals(mountPath)) + return mounts.get(mountPath); + if (path.startsWith(mountPath + JcrPath.separator)) { + res = mounts.get(mountPath); + // we keep the last one + } + } + assert res != null; + return res; + } + + public WorkspaceFileStore getFileStore(Node node) throws RepositoryException { + String workspaceName = node.getSession().getWorkspace().getName(); + if (workspaceName.equals(baseFileStore.getWorkspace().getName())) + return baseFileStore; + for (String mountPath : mounts.keySet()) { + WorkspaceFileStore fileStore = mounts.get(mountPath); + if (workspaceName.equals(fileStore.getWorkspace().getName())) + return fileStore; + } + throw new IllegalStateException("No workspace mount found for " + node + " in workspace " + workspaceName); + } + + public Iterator listDirectMounts(Path base) { + String baseStr = base.toString(); + Set res = new HashSet<>(); + mounts: for (String mountPath : mounts.keySet()) { + if (mountPath.equals(baseStr)) + continue mounts; + if (mountPath.startsWith(baseStr)) { + JcrPath path = new JcrPath(this, mountPath); + Path relPath = base.relativize(path); + if (relPath.getNameCount() == 1) + res.add(path); + } + } + return res.iterator(); + } + + public WorkspaceFileStore getBaseFileStore() { + return baseFileStore; + } + + @Override + public FileSystemProvider provider() { + return provider; + } + + @Override + public void close() throws IOException { + JcrUtils.logoutQuietly(session); + for (String mountPath : mounts.keySet()) { + WorkspaceFileStore fileStore = mounts.get(mountPath); + try { + fileStore.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public boolean isOpen() { + return session.isLive(); + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public String getSeparator() { + return JcrPath.separator; + } + + @Override + public Iterable getRootDirectories() { + Set single = new HashSet<>(); + single.add(new JcrPath(this, JcrPath.separator)); + return single; + } + + @Override + public Iterable getFileStores() { + List stores = new ArrayList<>(); + stores.add(baseFileStore); + stores.addAll(mounts.values()); + return stores; + } + + @Override + public Set supportedFileAttributeViews() { + try { + String[] prefixes = session.getNamespacePrefixes(); + Set 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; +// } + + public Repository getRepository() { + return repository; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java new file mode 100644 index 0000000..74d9a19 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFileSystemProvider.java @@ -0,0 +1,337 @@ +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.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.nodetype.NodeType; +import javax.jcr.nodetype.PropertyDefinition; + +import org.argeo.jcr.JcrUtils; + +/** Operations on a {@link JcrFileSystem}. */ +public abstract class JcrFileSystemProvider extends FileSystemProvider { + + @Override + public SeekableByteChannel newByteChannel(Path path, Set 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, path); + } catch (RepositoryException e) { + discardChanges(node); + throw new IOException("Cannot read file", e); + } + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, Filter filter) throws IOException { + try { + Node base = toNode(dir); + if (base == null) + throw new IOException(dir + " is not a JCR node"); + JcrFileSystem fileSystem = (JcrFileSystem) dir.getFileSystem(); + return new NodeDirectoryStream(fileSystem, base.getNodes(), fileSystem.listDirectMounts(dir), 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"); + Session session = parent.getSession(); + synchronized (session) { + 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); + save(session); + } + } 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(); + synchronized (session) { + session.refresh(false); + 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(); + } + save(session); + } + } 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 { + Session targetSession = targetNode.getSession(); + synchronized (targetSession) { + JcrUtils.copy(sourceNode, targetNode); + save(targetSession); + } + } 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 { + JcrFileSystem sourceFileSystem = (JcrFileSystem) source.getFileSystem(); + WorkspaceFileStore sourceStore = sourceFileSystem.getFileStore(source.toString()); + WorkspaceFileStore targetStore = sourceFileSystem.getFileStore(target.toString()); + try { + if (sourceStore.equals(targetStore)) { + sourceStore.getWorkspace().move(sourceStore.toJcrPath(source.toString()), + targetStore.toJcrPath(target.toString())); + } else { + // TODO implement it + throw new UnsupportedOperationException("Can only move paths within the same workspace."); + } + } catch (RepositoryException e) { + throw new IOException("Cannot move from " + source + " to " + target, e); + } + +// Node sourceNode = toNode(source); +// try { +// Session session = sourceNode.getSession(); +// synchronized (session) { +// session.move(sourceNode.getPath(), target.toString()); +// save(session); +// } +// } 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 path.getFileName().toString().charAt(0) == '.'; + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + JcrFileSystem fileSystem = (JcrFileSystem) path.getFileSystem(); + return fileSystem.getFileStore(path.toString()); + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + Node node = toNode(path); + if (node == null) + throw new NoSuchFileException(path + " does not exist"); + // TODO check access via JCR api + } + + @Override + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + @Override + public A readAttributes(Path path, Class 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 readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + try { + Node node = toNode(path); + String pattern = attributes.replace(',', '|'); + Map res = new HashMap(); + 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 { + Session session = node.getSession(); + synchronized (session) { + 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()); + } + save(session); + } + } 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? + } + } + + /** Make sure save is robust. */ + protected void save(Session session) throws RepositoryException { + session.refresh(true); + session.save(); + session.notifyAll(); + } + + /** + * To be overriden in order to support the ~ path, with an implementation + * specific concept of user home. + * + * @return null by default + */ + public Node getUserHome(Repository session) { + return null; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFsException.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFsException.java new file mode 100644 index 0000000..f214fdc --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrFsException.java @@ -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.cms.jcr/src/org/argeo/jcr/fs/JcrPath.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrPath.java new file mode 100644 index 0000000..7318b70 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/JcrPath.java @@ -0,0 +1,384 @@ +package org.argeo.jcr.fs; + +import java.nio.file.Path; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; + +import org.argeo.api.acr.fs.AbstractFsPath; + +/** A {@link Path} which contains a reference to a JCR {@link Node}. */ +public class JcrPath extends AbstractFsPath { + final static String separator = "/"; + final static char separatorChar = '/'; + +// private final JcrFileSystem fs; +// /** null for non absolute paths */ +// private final WorkspaceFileStore fileStore; +// private final String[] path;// null means root +// private final boolean absolute; +// +// // optim +// private final int hashCode; + + public JcrPath(JcrFileSystem filesSystem, String path) { + super(filesSystem, path); +// this.fs = filesSystem; +// if (path == null) +// throw new JcrFsException("Path cannot be null"); +// if (path.equals(separator)) {// root +// this.path = null; +// this.absolute = true; +// this.hashCode = 0; +// this.fileStore = fs.getBaseFileStore(); +// return; +// } else if (path.equals("")) {// empty path +// this.path = new String[] { "" }; +// this.absolute = false; +// this.fileStore = null; +// 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) == separatorChar ? true : false; +// +// this.fileStore = absolute ? fs.getFileStore(path) : null; +// +// String trimmedPath = path.substring(absolute ? 1 : 0, +// path.charAt(path.length() - 1) == separatorChar ? path.length() - 1 : path.length()); +// this.path = trimmedPath.split(separator); +// 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(); +// assert !(absolute && fileStore == null); + } + + public JcrPath(JcrFileSystem filesSystem, Node node) throws RepositoryException { + this(filesSystem, filesSystem.getFileStore(node).toFsPath(node)); + } + + /** Internal optimisation */ + private JcrPath(JcrFileSystem filesSystem, WorkspaceFileStore fileStore, String[] path, boolean absolute) { + super(filesSystem, fileStore, path, absolute); +// this.fs = filesSystem; +// this.path = path; +// this.absolute = path == null ? true : absolute; +// if (this.absolute && fileStore == null) +// throw new IllegalArgumentException("Absolute path requires a file store"); +// if (!this.absolute && fileStore != null) +// throw new IllegalArgumentException("A file store should not be provided for a relative path"); +// this.fileStore = fileStore; +// this.hashCode = path == null ? 0 : path[path.length - 1].hashCode(); +// assert !(absolute && fileStore == null); + } + + protected String cleanUpSegment(String segment) { + return Text.unescapeIllegalJcrChars(segment); + } + + @Override + protected JcrPath newInstance(String path) { + return new JcrPath(getFileSystem(), path); + } + + @Override + protected JcrPath newInstance(String[] segments, boolean absolute) { + return new JcrPath(getFileSystem(), getFileStore(), segments, absolute); + + } + +// @Override +// public FileSystem getFileSystem() { +// return fs; +// } +// +// @Override +// public boolean isAbsolute() { +// return absolute; +// } +// +// @Override +// public Path getRoot() { +// if (path == null) +// return this; +// return new JcrPath(fs, separator); +// } +// +// @Override +// public String toString() { +// return toFsPath(path); +// } +// +// private String toFsPath(String[] path) { +// 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(); +// } + +// @Deprecated +// private String toJcrPath() { +// return toJcrPath(path); +// } +// +// @Deprecated +// private String toJcrPath(String[] path) { +// 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, separator); +// String[] parentPath = Arrays.copyOfRange(path, 0, path.length - 1); +// if (!absolute) +// return new JcrPath(fs, null, parentPath, absolute); +// else +// return new JcrPath(fs, toFsPath(parentPath)); +// } +// +// @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, null, 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); +// } +// if (!absolute) +// return new JcrPath(fs, null, newPath, absolute); +// else { +// return new JcrPath(fs, toFsPath(newPath)); +// } +// } +// +// @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 iterator() { +// return new Iterator() { +// 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(fs.provider().getScheme(), 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, fileStore, path, true); +// } +// +// @Override +// public Path toRealPath(LinkOption... options) throws IOException { +// return this; +// } +// +// @Override +// public File toFile() { +// throw new UnsupportedOperationException(); +// } + + public Node getNode() throws RepositoryException { + if (!isAbsolute())// TODO default dir + throw new JcrFsException("Cannot get a JCR node from a relative path"); + assert getFileStore() != null; + return getFileStore().toNode(getSegments()); +// 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.cms.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java new file mode 100644 index 0000000..eda07a5 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeDirectoryStream.java @@ -0,0 +1,77 @@ +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 { + private final JcrFileSystem fs; + private final NodeIterator nodeIterator; + private final Iterator additionalPaths; + private final Filter filter; + + public NodeDirectoryStream(JcrFileSystem fs, NodeIterator nodeIterator, Iterator additionalPaths, + Filter filter) { + this.fs = fs; + this.nodeIterator = nodeIterator; + this.additionalPaths = additionalPaths; + this.filter = filter; + } + + @Override + public void close() throws IOException { + } + + @Override + public Iterator iterator() { + return new Iterator() { + 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; + if (fs.skipNode(node)) + 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); + } + } + + if (next == null) { + if (additionalPaths.hasNext()) + next = additionalPaths.next(); + } + + 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.cms.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java new file mode 100644 index 0000000..8054d52 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/NodeFileAttributes.java @@ -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.cms.jcr/src/org/argeo/jcr/fs/Text.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/Text.java new file mode 100644 index 0000000..4643c8c --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/Text.java @@ -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; + +/** + * Hacked from org.apache.jackrabbit.util.Text in Jackrabbit JCR Commons + * 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 true, 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 strings = new ArrayList(); + 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 oldString in text + * with newString. + * + * @param text + * @param oldString + * old substring to be replaced with newString + * @param newString + * new substring to replace occurrences of oldString + * @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("<"); + } else if (ch == '>') { + buf.append(">"); + } else if (ch == '&') { + buf.append("&"); + } else if (ch == '"') { + buf.append("""); + } else if (ch == '\'') { + buf.append(isHtml ? "'" : "'"); + } + } + 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 escape() + * and unescape() METHODS. They contains the characters as + * defined 'unreserved' in section 2.3 of the RFC 2396 'URI generic syntax': + *

+ * + *

+	 * unreserved  = alphanum | mark
+	 * mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
+	 * 
+ */ + 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 string using the + * escape 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 string is null. + */ + public static String escape(String string, char escape) { + return escape(string, escape, false); + } + + /** + * Does an URL encoding of the string using the + * escape 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 isPath is + * true, additionally the slash '/' is ignored, too. + * + * @param string + * the string to encode. + * @param escape + * the escape character. + * @param isPath + * if true, the string is treated as path + * @return the escaped string + * @throws NullPointerException + * if string is null. + */ + 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 string. 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 string is null. + */ + public static String escape(String string) { + return escape(string, '%'); + } + + /** + * Does a URL encoding of the path. 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 path is null. + */ + public static String escapePath(String path) { + return escape(path, '%', true); + } + + /** + * Does a URL decoding of the string using the + * escape 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 string is null. + * @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 string. 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 string is null. + * @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. + *

+ * QName EBNF:
+ *

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 *) + * + * @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. + *

+ * QName EBNF:
+ *

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 *) + * + * @since Apache Jackrabbit 2.3.2 and 2.2.10 + * @see
JCR-3128 + * @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. + *

+ * Example:
+ * 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. + *

+ * 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 null if path is + * null. + */ + public static String getName(String path) { + return getName(path, '/'); + } + + /** + * Returns the name part of the path, delimited by the given + * delim. If the given path is already a name (i.e. contains no + * delim characters) it is returned. + * + * @param path + * the path + * @param delim + * the delimiter + * @return the name part or null if path is + * null. + */ + 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 qname. If the + * prefix is missing, an empty string is returned. Please note, that this + * method does not validate the name or prefix. + *

+ * 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 qname is null + */ + public static String getNamespacePrefix(String qname) { + int pos = qname.indexOf(':'); + return pos >= 0 ? qname.substring(0, pos) : ""; + } + + /** + * Returns the local name of the given qname. Please note, that + * this method does not validate the name. + *

+ * the qname has the format: qname := [prefix ':'] local; + * + * @param qname + * a qualified name + * @return the localname + * + * @see #getNamespacePrefix(String) + * + * @throws NullPointerException + * if qname is null + */ + 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 descendant path is hierarchical a + * descendant of path. + * + * @param path + * the current path + * @param descendant + * the potential descendant + * @return true if the descendant is a descendant; + * false 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 descendant path is hierarchical a + * descendant of path or equal to it. + * + * @param path + * the path to check + * @param descendant + * the potential descendant + * @return true if the descendant is a descendant + * or equal; false 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 nth relative parent of the path, where n=level. + *

+ * Example:
+ * + * Text.getRelativeParent("/foo/bar/test", 1) == "/foo/bar" + * + * + * @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 nth absolute parent of the path, where n=level. + *

+ * Example:
+ * + * Text.getAbsoluteParent("/foo/bar/test", 1) == "/foo/bar" + * + * + * @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 + * ${...} 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 + * ignoreMissing is true. In the later case, the + * missing variable is replaced by the empty string. + * + * @param value + * the original value + * @param ignoreMissing + * if true, 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.cms.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java new file mode 100644 index 0000000..ce4205a --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/WorkspaceFileStore.java @@ -0,0 +1,192 @@ +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 java.util.Arrays; + +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.Workspace; + +import org.argeo.api.acr.fs.AbstractFsStore; +import org.argeo.jcr.JcrUtils; + +/** A {@link FileStore} implementation based on JCR {@link Workspace}. */ +public class WorkspaceFileStore extends AbstractFsStore { + private final String mountPath; + private final Workspace workspace; + private final String workspaceName; + private final int mountDepth; + + public WorkspaceFileStore(String mountPath, Workspace workspace) { + if ("/".equals(mountPath) || "".equals(mountPath)) + throw new IllegalArgumentException( + "Mount path '" + mountPath + "' is unsupported, use null for the base file store"); + if (mountPath != null && !mountPath.startsWith(JcrPath.separator)) + throw new IllegalArgumentException("Mount path '" + mountPath + "' cannot end with /"); + if (mountPath != null && mountPath.endsWith(JcrPath.separator)) + throw new IllegalArgumentException("Mount path '" + mountPath + "' cannot end with /"); + this.mountPath = mountPath; + if (mountPath == null) + mountDepth = 0; + else { + mountDepth = mountPath.split(JcrPath.separator).length - 1; + } + this.workspace = workspace; + this.workspaceName = workspace.getName(); + } + + public void close() { + JcrUtils.logoutQuietly(workspace.getSession()); + } + + @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 type) { + return false; + } + + @Override + public boolean supportsFileAttributeView(String name) { + return false; + } + + @Override + public V getFileStoreAttributeView(Class type) { + return null; + } + + @Override + public Object getAttribute(String attribute) throws IOException { + return workspace.getSession().getRepository().getDescriptor(attribute); + } + + public Workspace getWorkspace() { + return workspace; + } + + public String toFsPath(Node node) throws RepositoryException { + String nodeWorkspaceName = node.getSession().getWorkspace().getName(); + if (!nodeWorkspaceName.equals(workspace.getName())) + throw new IllegalArgumentException("Icompatible " + node + " from workspace '" + nodeWorkspaceName + + "' in file store '" + workspace.getName() + "'"); + return mountPath == null ? node.getPath() : mountPath + node.getPath(); + } + + public boolean isBase() { + return mountPath == null; + } + + Node toNode(String[] fullPath) throws RepositoryException { + String jcrPath = toJcrPath(fullPath); + Session session = workspace.getSession(); + if (!session.itemExists(jcrPath)) + return null; + Node node = session.getNode(jcrPath); + return node; + } + + String toJcrPath(String fsPath) { + if (fsPath.length() == 1) + return toJcrPath((String[]) null);// root + String[] arr = fsPath.substring(1).split("/"); +// if (arr.length == 0 || (arr.length == 1 && arr[0].equals(""))) +// return toJcrPath((String[]) null);// root +// else + return toJcrPath(arr); + } + + private String toJcrPath(String[] path) { + if (path == null) + return "/"; + if (path.length < mountDepth) + throw new IllegalArgumentException( + "Path " + Arrays.asList(path) + " is no compatible with mount " + mountPath); + + if (!isBase()) { + // check mount compatibility + StringBuilder mount = new StringBuilder(); + mount.append('/'); + for (int i = 0; i < mountDepth; i++) { + if (i != 0) + mount.append('/'); + mount.append(Text.escapeIllegalJcrChars(path[i])); + } + if (!mountPath.equals(mount.toString())) + throw new IllegalArgumentException( + "Path " + Arrays.asList(path) + " is no compatible with mount " + mountPath); + } + + StringBuilder sb = new StringBuilder(); + sb.append('/'); + for (int i = mountDepth; i < path.length; i++) { + if (i != mountDepth) + sb.append('/'); + sb.append(Text.escapeIllegalJcrChars(path[i])); + } + return sb.toString(); + } + + public String getMountPath() { + return mountPath; + } + + public String getWorkspaceName() { + return workspaceName; + } + + public int getMountDepth() { + return mountDepth; + } + + @Override + public int hashCode() { + return workspaceName.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof WorkspaceFileStore)) + return false; + WorkspaceFileStore other = (WorkspaceFileStore) obj; + return workspaceName.equals(other.workspaceName); + } + + @Override + public String toString() { + return "WorkspaceFileStore " + workspaceName; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/fs/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/package-info.java new file mode 100644 index 0000000..0cdfdaf --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/fs/package-info.java @@ -0,0 +1,2 @@ +/** Java NIO file system implementation based on plain JCR. */ +package org.argeo.jcr.fs; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/jcrx.cnd b/org.argeo.cms.jcr/src/org/argeo/jcr/jcrx.cnd new file mode 100644 index 0000000..3eb0e7a --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/jcrx.cnd @@ -0,0 +1,16 @@ +// +// JCR EXTENSIONS +// + + +[jcrx:xmlvalue] +- * ++ jcr:xmltext (jcrx:xmltext) = jcrx:xmltext + +[jcrx:xmltext] + - jcr:xmlcharacters (STRING) mandatory + +[jcrx:csum] +mixin + - jcrx:sum (STRING) * + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jcr/package-info.java new file mode 100644 index 0000000..1837749 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/package-info.java @@ -0,0 +1,2 @@ +/** Generic JCR utilities. */ +package org.argeo.jcr; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java new file mode 100644 index 0000000..0177636 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/AbstractUrlProxy.java @@ -0,0 +1,153 @@ +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.argeo.api.cms.CmsLog; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; + +/** Base class for URL based proxys. */ +public abstract class AbstractUrlProxy implements ResourceProxy { + private final static CmsLog log = CmsLog.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 (RepositoryException e) { + JcrUtils.discardQuietly(jcrAdminSession); + throw new JcrException("Cannot initialize URL 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 JcrException("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 JcrException("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_UNSTRUCTURED); + } 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, true); + 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.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java new file mode 100644 index 0000000..84eea1f --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxy.java @@ -0,0 +1,16 @@ +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, null if the resource was not found + * (e.g. HTTP 404) + */ + public Node proxy(String relativePath); +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java new file mode 100644 index 0000000..a8e00df --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/ResourceProxyServlet.java @@ -0,0 +1,115 @@ +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.argeo.jcr.JcrException; +import org.argeo.api.cms.CmsLog; +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 CmsLog log = CmsLog + .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); + + IOUtils.copy(in, response.getOutputStream()); + } catch (RepositoryException e) { + throw new JcrException("Cannot download " + node, e); + } catch (IOException e) { + throw new RuntimeException("Cannot download " + node, e); + } + } + + public void setProxy(ResourceProxy resourceProxy) { + this.proxy = resourceProxy; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/package-info.java b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/package-info.java new file mode 100644 index 0000000..a578c45 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/proxy/package-info.java @@ -0,0 +1,2 @@ +/** Components to build proxys based on JCR. */ +package org.argeo.jcr.proxy; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java b/org.argeo.cms.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java new file mode 100644 index 0000000..2adb6a9 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/xml/JcrXmlUtils.java @@ -0,0 +1,186 @@ +package org.argeo.jcr.xml; + +import java.io.IOException; +import java.io.Writer; +import java.util.Map; +import java.util.TreeMap; + +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.RepositoryException; +import javax.jcr.Value; +import javax.jcr.nodetype.NodeType; + +import org.argeo.jcr.Jcr; + +/** Utilities around JCR and XML. */ +public class JcrXmlUtils { + /** + * Convenience method calling {@link #toXmlElements(Writer, Node, boolean)} with + * false. + */ + public static void toXmlElements(Writer writer, Node node) throws RepositoryException, IOException { + toXmlElements(writer, node, null, false, false, false); + } + + /** + * Write JCR properties as XML elements in a tree structure whose elements are + * named by node primary type. + * + * @param writer the writer to use + * @param node the subtree + * @param depth maximal depth, or if null the whole + * subtree. It must be positive, with depth 0 + * describing just the node without its children. + * @param withMetadata whether to write the primary type and mixins as + * elements + * @param withPrefix whether to keep the namespace prefixes + * @param propertiesAsElements whether single properties should be written as + * elements rather than attributes. If + * false, multiple properties will be + * skipped. + */ + public static void toXmlElements(Writer writer, Node node, Integer depth, boolean withMetadata, boolean withPrefix, + boolean propertiesAsElements) throws RepositoryException, IOException { + if (depth != null && depth < 0) + throw new IllegalArgumentException("Depth " + depth + " is negative."); + + if (node.getName().equals(Jcr.JCR_XMLTEXT)) { + writer.write(node.getProperty(Jcr.JCR_XMLCHARACTERS).getString()); + return; + } + + if (!propertiesAsElements) { + Map attrs = new TreeMap<>(); + PropertyIterator pit = node.getProperties(); + properties: while (pit.hasNext()) { + Property p = pit.nextProperty(); + if (!p.isMultiple()) { + String pName = p.getName(); + if (!withMetadata && (pName.equals(Jcr.JCR_PRIMARY_TYPE) || pName.equals(Jcr.JCR_UUID) + || pName.equals(Jcr.JCR_CREATED) || pName.equals(Jcr.JCR_CREATED_BY) + || pName.equals(Jcr.JCR_LAST_MODIFIED) || pName.equals(Jcr.JCR_LAST_MODIFIED_BY))) + continue properties; + attrs.put(withPrefix(p.getName(), withPrefix), p.getString()); + } + } + if (withMetadata && node.hasProperty(Property.JCR_UUID)) + attrs.put("id", "urn:uuid:" + node.getProperty(Property.JCR_UUID).getString()); + attrs.put(withPrefix ? Jcr.JCR_NAME : "name", node.getName()); + writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix), attrs, node.hasNodes()); + } else { + if (withMetadata && node.hasProperty(Property.JCR_UUID)) { + writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix), "id", + "urn:uuid:" + node.getProperty(Property.JCR_UUID).getString()); + } else { + writeStart(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix)); + } + // name + writeStart(writer, withPrefix ? Jcr.JCR_NAME : "name"); + writer.append(node.getName()); + writeEnd(writer, withPrefix ? Jcr.JCR_NAME : "name"); + } + + // mixins + if (withMetadata) { + for (NodeType mixin : node.getMixinNodeTypes()) { + writeStart(writer, withPrefix ? Jcr.JCR_MIXIN_TYPES : "mixinTypes"); + writer.append(mixin.getName()); + writeEnd(writer, withPrefix ? Jcr.JCR_MIXIN_TYPES : "mixinTypes"); + } + } + + // properties as elements + if (propertiesAsElements) { + PropertyIterator pit = node.getProperties(); + properties: while (pit.hasNext()) { + Property p = pit.nextProperty(); + if (p.isMultiple()) { + for (Value value : p.getValues()) { + writeStart(writer, withPrefix(p.getName(), withPrefix)); + writer.write(value.getString()); + writeEnd(writer, withPrefix(p.getName(), withPrefix)); + } + } else { + Value value = p.getValue(); + String pName = p.getName(); + if (!withMetadata && (pName.equals(Jcr.JCR_PRIMARY_TYPE) || pName.equals(Jcr.JCR_UUID) + || pName.equals(Jcr.JCR_CREATED) || pName.equals(Jcr.JCR_CREATED_BY) + || pName.equals(Jcr.JCR_LAST_MODIFIED) || pName.equals(Jcr.JCR_LAST_MODIFIED_BY))) + continue properties; + writeStart(writer, withPrefix(p.getName(), withPrefix)); + writer.write(value.getString()); + writeEnd(writer, withPrefix(p.getName(), withPrefix)); + } + } + } + + // children + if (node.hasNodes()) { + if (depth == null || depth > 0) { + NodeIterator nit = node.getNodes(); + while (nit.hasNext()) { + toXmlElements(writer, nit.nextNode(), depth == null ? null : depth - 1, withMetadata, withPrefix, + propertiesAsElements); + } + } + writeEnd(writer, withPrefix(node.getPrimaryNodeType().getName(), withPrefix)); + } + } + + private static String withPrefix(String str, boolean withPrefix) { + if (withPrefix) + return str; + int index = str.indexOf(':'); + if (index < 0) + return str; + return str.substring(index + 1); + } + + private static void writeStart(Writer writer, String tagName) throws IOException { + writer.append('<'); + writer.append(tagName); + writer.append('>'); + } + + private static void writeStart(Writer writer, String tagName, String attr, String value) throws IOException { + writer.append('<'); + writer.append(tagName); + writer.append(' '); + writer.append(attr); + writer.append("=\""); + writer.append(value); + writer.append("\">"); + } + + private static void writeStart(Writer writer, String tagName, Map attrs, boolean hasChildren) + throws IOException { + writer.append('<'); + writer.append(tagName); + for (String attr : attrs.keySet()) { + writer.append(' '); + writer.append(attr); + writer.append("=\""); + writer.append(attrs.get(attr)); + writer.append('\"'); + } + if (hasChildren) + writer.append('>'); + else + writer.append("/>"); + } + + private static void writeEnd(Writer writer, String tagName) throws IOException { + writer.append("'); + } + + /** Singleton. */ + private JcrXmlUtils() { + + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl b/org.argeo.cms.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl new file mode 100644 index 0000000..813d065 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/jcr/xml/removePrefixes.xsl @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java new file mode 100644 index 0000000..977adac --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/AbstractMaintenanceService.java @@ -0,0 +1,232 @@ +package org.argeo.maintenance; + +import java.io.IOException; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.argeo.api.acr.spi.ProvidedRepository; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrUtils; +import org.argeo.util.naming.Distinguished; +import org.argeo.util.transaction.WorkTransaction; +import org.osgi.service.useradmin.Group; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.UserAdmin; + +/** Make sure roles and access rights are properly configured. */ +public abstract class AbstractMaintenanceService { + private final static CmsLog log = CmsLog.getLog(AbstractMaintenanceService.class); + + private Repository repository; +// private UserAdminService userAdminService; + private UserAdmin userAdmin; + private WorkTransaction userTransaction; + + private ProvidedRepository contentRepository; + + public void init() { + makeSureRolesExists(getRequiredRoles()); + configureStandardRoles(); + + Set workspaceNames = getWorkspaceNames(); + if (workspaceNames == null || workspaceNames.isEmpty()) { + configureJcr(repository, null); + } else { + for (String workspaceName : workspaceNames) + configureJcr(repository, workspaceName); + } + } + + /** Configures a workspace. */ + protected void configureJcr(Repository repository, String workspaceName) { + Session adminSession; + try { + adminSession = CmsJcrUtils.openDataAdminSession(repository, workspaceName); + } catch (RuntimeException e1) { + if (e1.getCause() != null && e1.getCause() instanceof NoSuchWorkspaceException) { + Session defaultAdminSession = CmsJcrUtils.openDataAdminSession(repository, null); + try { + defaultAdminSession.getWorkspace().createWorkspace(workspaceName); + log.info("Created JCR workspace " + workspaceName); + } catch (RepositoryException e) { + throw new IllegalStateException("Cannot create workspace " + workspaceName, e); + } finally { + Jcr.logout(defaultAdminSession); + } + adminSession = CmsJcrUtils.openDataAdminSession(repository, workspaceName); + } else + throw e1; + } + try { + if (prepareJcrTree(adminSession)) { + configurePrivileges(adminSession); + } + } catch (RepositoryException | IOException e) { + throw new IllegalStateException("Cannot initialise JCR data layer.", e); + } finally { + JcrUtils.logoutQuietly(adminSession); + } + } + + /** To be overridden. */ + protected Set getWorkspaceNames() { + return null; + } + + /** + * To be overridden in order to programmatically set relationships between + * roles. Does nothing by default. + */ + protected void configureStandardRoles() { + } + + /** + * Creates the base JCR tree structure expected for this app if necessary. + * + * Expects a clean session ({@link Session#hasPendingChanges()} should return + * false) and saves it once the changes have been done. Thus the session can be + * rolled back if an exception occurs. + * + * @return true if something as been updated + */ + public boolean prepareJcrTree(Session adminSession) throws RepositoryException, IOException { + return false; + } + + /** + * Adds app specific default privileges. + * + * Expects a clean session ({@link Session#hasPendingChanges()} should return + * false} and saves it once the changes have been done. Thus the session can be + * rolled back if an exception occurs. + * + * Warning: no check is done and corresponding privileges are always added, so + * only call this when necessary + */ + public void configurePrivileges(Session session) throws RepositoryException { + } + + /** The system roles that must be available in the system. */ + protected Set getRequiredRoles() { + return new HashSet<>(); + } + + public void destroy() { + + } + + /* + * UTILITIES + */ + + /** Create these roles as group if they don't exist. */ + protected void makeSureRolesExists(EnumSet enumSet) { + makeSureRolesExists(Distinguished.enumToDns(enumSet)); + } + + /** Create these roles as group if they don't exist. */ + protected void makeSureRolesExists(Set requiredRoles) { + if (requiredRoles == null) + return; + if (getUserAdmin() == null) { + log.warn("No user admin service available, cannot make sure that role exists"); + return; + } + for (String role : requiredRoles) { + Role systemRole = getUserAdmin().getRole(role); + if (systemRole == null) { + try { + getUserTransaction().begin(); + getUserAdmin().createRole(role, Role.GROUP); + getUserTransaction().commit(); + log.info("Created role " + role); + } catch (Exception e) { + try { + getUserTransaction().rollback(); + } catch (Exception e1) { + // silent + } + throw new IllegalStateException("Cannot create role " + role, e); + } + } + } + } + + /** Add a user or group to a group. */ + protected void addToGroup(String groupToAddDn, String groupDn) { + if (groupToAddDn.contentEquals(groupDn)) { + if (log.isTraceEnabled()) + log.trace("Ignore adding group " + groupDn + " to itself"); + return; + } + + if (getUserAdmin() == null) { + log.warn("No user admin service available, cannot add group " + groupToAddDn + " to " + groupDn); + return; + } + Group groupToAdd = (Group) getUserAdmin().getRole(groupToAddDn); + if (groupToAdd == null) + throw new IllegalArgumentException("Group " + groupToAddDn + " not found"); + Group group = (Group) getUserAdmin().getRole(groupDn); + if (group == null) + throw new IllegalArgumentException("Group " + groupDn + " not found"); + try { + getUserTransaction().begin(); + if (group.addMember(groupToAdd)) + log.info("Added " + groupToAddDn + " to " + group); + getUserTransaction().commit(); + } catch (Exception e) { + try { + getUserTransaction().rollback(); + } catch (Exception e1) { + // silent + } + throw new IllegalStateException("Cannot add " + groupToAddDn + " to " + groupDn); + } + } + + /* + * DEPENDENCY INJECTION + */ + public void setRepository(Repository repository) { + this.repository = repository; + } + +// public void setUserAdminService(UserAdminService userAdminService) { +// this.userAdminService = userAdminService; +// } + + protected WorkTransaction getUserTransaction() { + return userTransaction; + } + + protected UserAdmin getUserAdmin() { + return userAdmin; + } + + public void setUserAdmin(UserAdmin userAdmin) { + this.userAdmin = userAdmin; + } + + public void setUserTransaction(WorkTransaction userTransaction) { + this.userTransaction = userTransaction; + } + + public void setContentRepository(ProvidedRepository contentRepository) { + this.contentRepository = contentRepository; + } + + protected ProvidedRepository getContentRepository() { + return contentRepository; + } + + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/SimpleRoleRegistration.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/SimpleRoleRegistration.java new file mode 100644 index 0000000..e65e46a --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/SimpleRoleRegistration.java @@ -0,0 +1,86 @@ +package org.argeo.maintenance; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +import org.argeo.api.cms.CmsLog; +import org.argeo.util.transaction.WorkTransaction; +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 CmsLog log = CmsLog.getLog(SimpleRoleRegistration.class); + + private String role; + private List roles = new ArrayList(); + private UserAdmin userAdmin; + private WorkTransaction 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 IllegalArgumentException("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 IllegalArgumentException("Badly formatted role name " + name, e); + } + } + + public void setRole(String role) { + this.role = role; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public void setUserAdmin(UserAdmin userAdminService) { + this.userAdmin = userAdminService; + } + + public void setUserTransaction(WorkTransaction userTransaction) { + this.userTransaction = userTransaction; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/BackupContentHandler.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/BackupContentHandler.java new file mode 100644 index 0000000..ef83c1f --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/BackupContentHandler.java @@ -0,0 +1,257 @@ +package org.argeo.maintenance.backup; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.util.Arrays; +import java.util.Base64; +import java.util.Set; +import java.util.TreeSet; + +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** XML handler serialising a JCR system view. */ +public class BackupContentHandler extends DefaultHandler { + final static int MAX_DEPTH = 1024; + final static String SV_NAMESPACE_URI = "http://www.jcp.org/jcr/sv/1.0"; + final static String SV_PREFIX = "sv"; + // elements + final static String NODE = "node"; + final static String PROPERTY = "property"; + final static String VALUE = "value"; + // attributes + final static String NAME = "name"; + final static String MULTIPLE = "multiple"; + final static String TYPE = "type"; + + // values + final static String BINARY = "Binary"; + final static String JCR_CONTENT = "jcr:content"; + + private Writer out; + private Session session; + private Set contentPaths = new TreeSet<>(); + + boolean prettyPrint = true; + + private final String parentPath; + +// private boolean inSystem = false; + + public BackupContentHandler(Writer out, Node node) { + super(); + this.out = out; + this.session = Jcr.getSession(node); + parentPath = Jcr.getParentPath(node); + } + + private int currentDepth = -1; + private String[] currentPath = new String[MAX_DEPTH]; + + private boolean currentPropertyIsMultiple = false; + private String currentEncoded = null; + private Base64.Encoder base64encore = Base64.getEncoder(); + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + boolean isNode; + boolean isProperty; + switch (localName) { + case NODE: + isNode = true; + isProperty = false; + break; + case PROPERTY: + isNode = false; + isProperty = true; + break; + default: + isNode = false; + isProperty = false; + } + + if (isNode) { + String nodeName = attributes.getValue(SV_NAMESPACE_URI, NAME); + currentDepth = currentDepth + 1; +// if (currentDepth >= 0) + currentPath[currentDepth] = nodeName; +// System.out.println(getCurrentPath() + " , depth=" + currentDepth); +// if ("jcr:system".equals(nodeName)) { +// inSystem = true; +// } + } +// if (inSystem) +// return; + + if (SV_NAMESPACE_URI.equals(uri)) + try { + if (prettyPrint) { + if (isNode) { + out.write(spaces()); + out.write("\n"); + out.write(spaces()); + } else if (isProperty) + out.write(spaces()); + else if (currentPropertyIsMultiple) + out.write(spaces()); + } + + out.write("<"); + out.write(SV_PREFIX + ":" + localName); + if (isProperty) + currentPropertyIsMultiple = false; // always reset + for (int i = 0; i < attributes.getLength(); i++) { + String ns = attributes.getURI(i); + if (SV_NAMESPACE_URI.equals(ns)) { + String attrName = attributes.getLocalName(i); + String attrValue = attributes.getValue(i); + out.write(" "); + out.write(SV_PREFIX + ":" + attrName); + out.write("="); + out.write("\""); + out.write(attrValue); + out.write("\""); + if (isProperty) { + if (MULTIPLE.equals(attrName)) + currentPropertyIsMultiple = Boolean.parseBoolean(attrValue); + else if (TYPE.equals(attrName)) { + if (BINARY.equals(attrValue)) { + if (JCR_CONTENT.equals(getCurrentName())) { + contentPaths.add(getCurrentJcrPath()); + } else { + Binary binary = session.getNode(getCurrentJcrPath()).getProperty(attrName) + .getBinary(); + try (InputStream in = binary.getStream()) { + currentEncoded = base64encore.encodeToString(IOUtils.toByteArray(in)); + } finally { + + } + } + } + } + } + } + } + if (isNode && currentDepth == 0) { + // out.write(" xmlns=\"" + SV_NAMESPACE_URI + "\""); + out.write(" xmlns:" + SV_PREFIX + "=\"" + SV_NAMESPACE_URI + "\""); + } + out.write(">"); + + if (prettyPrint) + if (isNode) + out.write("\n"); + else if (isProperty && currentPropertyIsMultiple) + out.write("\n"); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + boolean isNode = localName.equals(NODE); + boolean isValue = localName.equals(VALUE); + if (prettyPrint) + if (!isValue) + try { + if (isNode || currentPropertyIsMultiple) + out.write(spaces()); + } catch (IOException e1) { + throw new RuntimeException(e1); + } + if (isNode) { +// System.out.println("endElement " + getCurrentPath() + " , depth=" + currentDepth); +// if (currentDepth > 0) + currentPath[currentDepth] = null; + currentDepth = currentDepth - 1; +// if (inSystem) { +// // System.out.println("Skip " + getCurrentPath()+" , +// // currentDepth="+currentDepth); +// if (currentDepth == 0) { +// inSystem = false; +// return; +// } +// } + } +// if (inSystem) +// return; + if (SV_NAMESPACE_URI.equals(uri)) + try { + if (isValue && currentEncoded != null) { + out.write(currentEncoded); + } + currentEncoded = null; + out.write(""); + if (prettyPrint) + if (!isValue) + out.write("\n"); + else { + if (currentPropertyIsMultiple) + out.write("\n"); + } + if (currentDepth == 0) + out.flush(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + private char[] spaces() { + char[] arr = new char[currentDepth]; + Arrays.fill(arr, ' '); + return arr; + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { +// if (inSystem) +// return; + try { + out.write(ch, start, length); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected String getCurrentName() { + assert currentDepth >= 0; +// if (currentDepth == 0) +// return "jcr:root"; + return currentPath[currentDepth]; + } + + protected String getCurrentJcrPath() { +// if (currentDepth == 0) +// return "/"; + StringBuilder sb = new StringBuilder(parentPath.equals("/") ? "" : parentPath); + for (int i = 0; i <= currentDepth; i++) { +// if (i != 0) + sb.append('/'); + sb.append(currentPath[i]); + } + return sb.toString(); + } + + public Set getContentPaths() { + return contentPaths; + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalBackup.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalBackup.java new file mode 100644 index 0000000..00d4be8 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalBackup.java @@ -0,0 +1,448 @@ +package org.argeo.maintenance.backup; + +import java.io.BufferedWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +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.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipOutputStream; + +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.RepositoryFactory; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.JackrabbitValue; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +/** + * Performs a backup of the data based only on programmatic interfaces. Useful + * for migration or live backup. Physical backups of the underlying file + * systems, databases, LDAP servers, etc. should be performed for disaster + * recovery. + */ +public class LogicalBackup implements Runnable { + private final static CmsLog log = CmsLog.getLog(LogicalBackup.class); + + public final static String WORKSPACES_BASE = "workspaces/"; + public final static String FILES_BASE = "files/"; + public final static String OSGI_BASE = "share/osgi/"; + + public final static String JCR_SYSTEM = "jcr:system"; + public final static String JCR_VERSION_STORAGE_PATH = "/jcr:system/jcr:versionStorage"; + + private final Repository repository; + private String defaultWorkspace; + private final BundleContext bundleContext; + + private final ZipOutputStream zout; + private final Path basePath; + + private ExecutorService executorService; + + private boolean performSoftwareBackup = false; + + private Map checksums = new TreeMap<>(); + + private int threadCount = 5; + + private boolean backupFailed = false; + + public LogicalBackup(BundleContext bundleContext, Repository repository, Path basePath) { + this.repository = repository; + this.zout = null; + this.basePath = basePath; + this.bundleContext = bundleContext; + } + + @Override + public void run() { + try { + log.info("Start logical backup to " + basePath); + perform(); + } catch (Exception e) { + log.error("Unexpected exception when performing logical backup", e); + throw new IllegalStateException("Logical backup failed", e); + } + + } + + public void perform() throws RepositoryException, IOException { + if (executorService != null && !executorService.isTerminated()) + throw new IllegalStateException("Another backup is running"); + executorService = Executors.newFixedThreadPool(threadCount); + long begin = System.currentTimeMillis(); + // software backup + if (bundleContext != null && performSoftwareBackup) + executorService.submit(() -> performSoftwareBackup(bundleContext)); + + // data backup + Session defaultSession = login(null); + defaultWorkspace = defaultSession.getWorkspace().getName(); + try { + String[] workspaceNames = defaultSession.getWorkspace().getAccessibleWorkspaceNames(); + workspaces: for (String workspaceName : workspaceNames) { + if ("security".equals(workspaceName)) + continue workspaces; + performDataBackup(workspaceName); + } + } finally { + JcrUtils.logoutQuietly(defaultSession); + executorService.shutdown(); + try { + executorService.awaitTermination(24, TimeUnit.HOURS); + } catch (InterruptedException e) { + // silent + throw new IllegalStateException("Backup was interrupted before completion", e); + } + } + // versions + executorService = Executors.newFixedThreadPool(threadCount); + try { + performVersionsBackup(); + } finally { + executorService.shutdown(); + try { + executorService.awaitTermination(24, TimeUnit.HOURS); + } catch (InterruptedException e) { + // silent + throw new IllegalStateException("Backup was interrupted before completion", e); + } + } + long duration = System.currentTimeMillis() - begin; + if (isBackupFailed()) + log.info("System logical backup failed after " + (duration / 60000) + "min " + (duration / 1000) + "s"); + else + log.info("System logical backup completed in " + (duration / 60000) + "min " + (duration / 1000) + "s"); + } + + protected void performDataBackup(String workspaceName) throws RepositoryException, IOException { + Session session = login(workspaceName); + try { + nodes: for (NodeIterator nit = session.getRootNode().getNodes(); nit.hasNext();) { + if (isBackupFailed()) + return; + Node nodeToExport = nit.nextNode(); + if (JCR_SYSTEM.equals(nodeToExport.getName())) + continue nodes; + String nodePath = nodeToExport.getPath(); + Future> contentPathsFuture = executorService + .submit(() -> performNodeBackup(workspaceName, nodePath)); + executorService.submit(() -> performFilesBackup(workspaceName, contentPathsFuture)); + } + } finally { + Jcr.logout(session); + } + } + + protected void performVersionsBackup() throws RepositoryException, IOException { + Session session = login(defaultWorkspace); + Node versionStorageNode = session.getNode(JCR_VERSION_STORAGE_PATH); + try { + for (NodeIterator nit = versionStorageNode.getNodes(); nit.hasNext();) { + Node nodeToExport = nit.nextNode(); + String nodePath = nodeToExport.getPath(); + if (isBackupFailed()) + return; + Future> contentPathsFuture = executorService + .submit(() -> performNodeBackup(defaultWorkspace, nodePath)); + executorService.submit(() -> performFilesBackup(defaultWorkspace, contentPathsFuture)); + } + } finally { + Jcr.logout(session); + } + + } + + protected Set performNodeBackup(String workspaceName, String nodePath) { + Session session = login(workspaceName); + try { + Node nodeToExport = session.getNode(nodePath); +// String nodeName = nodeToExport.getName(); +// if (nodeName.startsWith("jcr:") || nodeName.startsWith("rep:")) +// continue nodes; +// // TODO make it more robust / configurable +// if (nodeName.equals("user")) +// continue nodes; + String relativePath = WORKSPACES_BASE + workspaceName + nodePath + ".xml"; + OutputStream xmlOut = openOutputStream(relativePath); + BackupContentHandler contentHandler; + try (Writer writer = new BufferedWriter(new OutputStreamWriter(xmlOut, StandardCharsets.UTF_8))) { + contentHandler = new BackupContentHandler(writer, nodeToExport); + session.exportSystemView(nodeToExport.getPath(), contentHandler, true, false); + if (log.isDebugEnabled()) + log.debug(workspaceName + ":" + nodePath + " metadata exported to " + relativePath); + } + + // Files + Set contentPaths = contentHandler.getContentPaths(); + return contentPaths; + } catch (Exception e) { + markBackupFailed("Cannot backup node " + workspaceName + ":" + nodePath, e); + throw new ThreadDeath(); + } finally { + Jcr.logout(session); + } + } + + protected void performFilesBackup(String workspaceName, Future> contentPathsFuture) { + Set contentPaths; + try { + contentPaths = contentPathsFuture.get(24, TimeUnit.HOURS); + } catch (InterruptedException | ExecutionException | TimeoutException e1) { + markBackupFailed("Cannot retrieve content paths for workspace " + workspaceName, e1); + return; + } + if (contentPaths == null || contentPaths.size() == 0) + return; + Session session = login(workspaceName); + try { + String workspacesFilesBasePath = FILES_BASE + workspaceName; + for (String path : contentPaths) { + if (isBackupFailed()) + return; + Node contentNode = session.getNode(path); + Binary binary = null; + try { + binary = contentNode.getProperty(Property.JCR_DATA).getBinary(); + String fileRelativePath = workspacesFilesBasePath + contentNode.getParent().getPath(); + + // checksum + boolean skip = false; + String checksum = null; + if (session instanceof JackrabbitSession) { + JackrabbitValue value = (JackrabbitValue) contentNode.getProperty(Property.JCR_DATA).getValue(); +// ReferenceBinary referenceBinary = (ReferenceBinary) binary; + checksum = value.getContentIdentity(); + } + if (checksum != null) { + if (!checksums.containsKey(checksum)) { + checksums.put(checksum, fileRelativePath); + } else { + skip = true; + String sourcePath = checksums.get(checksum); + if (log.isTraceEnabled()) + log.trace(fileRelativePath + " : already " + sourcePath + " with checksum " + checksum); + createLink(sourcePath, fileRelativePath); + try (Writer writerSum = new OutputStreamWriter( + openOutputStream(fileRelativePath + ".sha256"), StandardCharsets.UTF_8)) { + writerSum.write(checksum); + } + } + } + + // copy file + if (!skip) + try (InputStream in = binary.getStream(); + OutputStream out = openOutputStream(fileRelativePath)) { + IOUtils.copy(in, out); + if (log.isTraceEnabled()) + log.trace("Workspace " + workspaceName + ": file content exported to " + + fileRelativePath); + } + } finally { + JcrUtils.closeQuietly(binary); + } + } + if (log.isDebugEnabled()) + log.debug(workspaceName + ":" + contentPaths.size() + " files exported to " + workspacesFilesBasePath); + } catch (Exception e) { + markBackupFailed("Cannot backup files from " + workspaceName + ":", e); + } finally { + Jcr.logout(session); + } + } + + protected OutputStream openOutputStream(String relativePath) throws IOException { + if (zout != null) { + ZipEntry entry = new ZipEntry(relativePath); + zout.putNextEntry(entry); + return zout; + } else if (basePath != null) { + Path targetPath = basePath.resolve(Paths.get(relativePath)); + Files.createDirectories(targetPath.getParent()); + return Files.newOutputStream(targetPath); + } else { + throw new UnsupportedOperationException(); + } + } + + protected void createLink(String source, String target) throws IOException { + if (zout != null) { + // TODO implement for zip + throw new UnsupportedOperationException(); + } else if (basePath != null) { + Path sourcePath = basePath.resolve(Paths.get(source)); + Path targetPath = basePath.resolve(Paths.get(target)); + Path relativeSource = targetPath.getParent().relativize(sourcePath); + Files.createDirectories(targetPath.getParent()); + Files.createSymbolicLink(targetPath, relativeSource); + } else { + throw new UnsupportedOperationException(); + } + } + + protected void closeOutputStream(String relativePath, OutputStream out) throws IOException { + if (zout != null) { + zout.closeEntry(); + } else if (basePath != null) { + out.close(); + } else { + throw new UnsupportedOperationException(); + } + } + + protected Session login(String workspaceName) { + if (bundleContext != null) {// local + return CmsJcrUtils.openDataAdminSession(repository, workspaceName); + } else {// remote + try { + return repository.login(workspaceName); + } catch (RepositoryException e) { + throw new JcrException(e); + } + } + } + + public final static void main(String[] args) throws Exception { + if (args.length == 0) { + printUsage("No argument"); + System.exit(1); + } + URI uri = new URI(args[0]); + Repository repository = createRemoteRepository(uri); + Path basePath = args.length > 1 ? Paths.get(args[1]) : Paths.get(System.getProperty("user.dir")); + if (!Files.exists(basePath)) + Files.createDirectories(basePath); + LogicalBackup backup = new LogicalBackup(null, repository, basePath); + backup.run(); + } + + private static void printUsage(String errorMessage) { + if (errorMessage != null) + System.err.println(errorMessage); + System.out.println("Usage: LogicalBackup []"); + + } + + protected static Repository createRemoteRepository(URI uri) throws RepositoryException { + RepositoryFactory repositoryFactory = new ClientDavexRepositoryFactory(); + Map params = new HashMap(); + params.put(ClientDavexRepositoryFactory.JACKRABBIT_DAVEX_URI, uri.toString()); + // TODO make it configurable + params.put(ClientDavexRepositoryFactory.JACKRABBIT_REMOTE_DEFAULT_WORKSPACE, CmsConstants.SYS_WORKSPACE); + return repositoryFactory.getRepository(params); + } + + public void performSoftwareBackup(BundleContext bundleContext) { + String bootBasePath = OSGI_BASE + "boot"; + Bundle[] bundles = bundleContext.getBundles(); + for (Bundle bundle : bundles) { + String relativePath = bootBasePath + "/" + bundle.getSymbolicName() + ".jar"; + Dictionary headers = bundle.getHeaders(); + Manifest manifest = new Manifest(); + Enumeration headerKeys = headers.keys(); + while (headerKeys.hasMoreElements()) { + String headerKey = headerKeys.nextElement(); + String headerValue = headers.get(headerKey); + manifest.getMainAttributes().putValue(headerKey, headerValue); + } + try (JarOutputStream jarOut = new JarOutputStream(openOutputStream(relativePath), manifest)) { + Enumeration resourcePaths = bundle.findEntries("/", "*", true); + resources: while (resourcePaths.hasMoreElements()) { + URL entryUrl = resourcePaths.nextElement(); + String entryPath = entryUrl.getPath(); + if (entryPath.equals("")) + continue resources; + if (entryPath.endsWith("/")) + continue resources; + String entryName = entryPath.substring(1);// remove first '/' + if (entryUrl.getPath().equals("/META-INF/")) + continue resources; + if (entryUrl.getPath().equals("/META-INF/MANIFEST.MF")) + continue resources; + // dev + if (entryUrl.getPath().startsWith("/target")) + continue resources; + if (entryUrl.getPath().startsWith("/src")) + continue resources; + if (entryUrl.getPath().startsWith("/ext")) + continue resources; + + if (entryName.startsWith("bin/")) {// dev + entryName = entryName.substring("bin/".length()); + } + + ZipEntry entry = new ZipEntry(entryName); + try (InputStream in = entryUrl.openStream()) { + try { + jarOut.putNextEntry(entry); + } catch (ZipException e) {// duplicate + continue resources; + } + IOUtils.copy(in, jarOut); + jarOut.closeEntry(); +// log.info(entryUrl); + } catch (FileNotFoundException e) { + log.warn(entryUrl + ": " + e.getMessage()); + } + } + } catch (IOException e1) { + throw new RuntimeException("Cannot export bundle " + bundle, e1); + } + } + if (log.isDebugEnabled()) + log.debug(bundles.length + " OSGi bundles exported to " + bootBasePath); + + } + + protected synchronized void markBackupFailed(Object message, Exception e) { + log.error(message, e); + backupFailed = true; + notifyAll(); + if (executorService != null) + executorService.shutdownNow(); + } + + protected boolean isBackupFailed() { + return backupFailed; + } +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalRestore.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalRestore.java new file mode 100644 index 0000000..122c967 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/LogicalRestore.java @@ -0,0 +1,85 @@ +package org.argeo.maintenance.backup; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; + +import javax.jcr.ImportUUIDBehavior; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.cms.jcr.CmsJcrUtils; +import org.argeo.jcr.Jcr; +import org.argeo.jcr.JcrException; +import org.argeo.jcr.JcrUtils; +import org.osgi.framework.BundleContext; + +/** Restores a backup in the format defined by {@link LogicalBackup}. */ +public class LogicalRestore implements Runnable { + private final static CmsLog log = CmsLog.getLog(LogicalRestore.class); + + private final Repository repository; + private final BundleContext bundleContext; + private final Path basePath; + + public LogicalRestore(BundleContext bundleContext, Repository repository, Path basePath) { + this.repository = repository; + this.basePath = basePath; + this.bundleContext = bundleContext; + } + + @Override + public void run() { + Path workspaces = basePath.resolve(LogicalBackup.WORKSPACES_BASE); + try { + // import jcr:system first +// Session defaultSession = NodeUtils.openDataAdminSession(repository, null); +// try (DirectoryStream xmls = Files.newDirectoryStream( +// workspaces.resolve(NodeConstants.SYS_WORKSPACE + LogicalBackup.JCR_VERSION_STORAGE_PATH), +// "*.xml")) { +// for (Path xml : xmls) { +// try (InputStream in = Files.newInputStream(xml)) { +// defaultSession.getWorkspace().importXML(LogicalBackup.JCR_VERSION_STORAGE_PATH, in, +// ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING); +// if (log.isDebugEnabled()) +// log.debug("Restored " + xml + " to " + defaultSession.getWorkspace().getName() + ":"); +// } +// } +// } finally { +// Jcr.logout(defaultSession); +// } + + // non-system content + try (DirectoryStream workspaceDirs = Files.newDirectoryStream(workspaces)) { + for (Path workspacePath : workspaceDirs) { + String workspaceName = workspacePath.getFileName().toString(); + Session session = JcrUtils.loginOrCreateWorkspace(repository, workspaceName); + try (DirectoryStream xmls = Files.newDirectoryStream(workspacePath, "*.xml")) { + xmls: for (Path xml : xmls) { + if (xml.getFileName().toString().startsWith("rep:")) + continue xmls; + try (InputStream in = Files.newInputStream(xml)) { + session.getWorkspace().importXML("/", in, + ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING); + if (log.isDebugEnabled()) + log.debug("Restored " + xml + " to workspace " + workspaceName); + } + } + } finally { + Jcr.logout(session); + } + } + } + } catch (IOException e) { + throw new RuntimeException("Cannot restore backup from " + basePath, e); + } catch (RepositoryException e) { + throw new JcrException("Cannot restore backup from " + basePath, e); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/package-info.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/package-info.java new file mode 100644 index 0000000..a61e19b --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/backup/package-info.java @@ -0,0 +1,2 @@ +/** Argeo Node backup utilities. */ +package org.argeo.maintenance.backup; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/internal/Activator.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/internal/Activator.java new file mode 100644 index 0000000..ef40ab3 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/internal/Activator.java @@ -0,0 +1,27 @@ +package org.argeo.maintenance.internal; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import javax.jcr.Repository; + +import org.argeo.maintenance.backup.LogicalBackup; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +public class Activator implements BundleActivator { + + @Override + public void start(BundleContext context) throws Exception { + // Start backup + Repository repository = context.getService(context.getServiceReference(Repository.class)); + Path basePath = Paths.get(System.getProperty("user.dir"), "backup"); + LogicalBackup backup = new LogicalBackup(context, repository, basePath); + backup.run(); + } + + @Override + public void stop(BundleContext context) throws Exception { + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/maintenance/package-info.java b/org.argeo.cms.jcr/src/org/argeo/maintenance/package-info.java new file mode 100644 index 0000000..1ce974c --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/maintenance/package-info.java @@ -0,0 +1,2 @@ +/** Utilities for the maintenance of an Argeo Node. */ +package org.argeo.maintenance; \ No newline at end of file diff --git a/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java new file mode 100644 index 0000000..bffe531 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAccessControlProvider.java @@ -0,0 +1,30 @@ +package org.argeo.security.jackrabbit; + +import java.security.Principal; +import java.util.Map; +import java.util.Set; + +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"); + if (!configuration.containsKey(PARAM_OMIT_DEFAULT_PERMISSIONS)) + configuration.put(PARAM_OMIT_DEFAULT_PERMISSIONS, "true"); + super.init(systemSession, configuration); + } + + @Override + public boolean canAccessRoot(Set principals) throws RepositoryException { + return super.canAccessRoot(principals); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAccessManager.java b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAccessManager.java new file mode 100644 index 0000000..7464078 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAccessManager.java @@ -0,0 +1,35 @@ +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.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAuthContext.java b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAuthContext.java new file mode 100644 index 0000000..d679c45 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoAuthContext.java @@ -0,0 +1,37 @@ +package org.argeo.security.jackrabbit; + +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +import org.apache.jackrabbit.core.security.authentication.AuthContext; + +/** Wraps a regular {@link LoginContext}, using the proper class loader. */ +class ArgeoAuthContext implements AuthContext { + private LoginContext lc; + + public ArgeoAuthContext(String appName, Subject subject, CallbackHandler callbackHandler) { + try { + lc = new LoginContext(appName, subject, callbackHandler); + } catch (LoginException e) { + throw new IllegalStateException("Cannot configure Jackrabbit login context", e); + } + } + + @Override + public void login() throws LoginException { + lc.login(); + } + + @Override + public Subject getSubject() { + return lc.getSubject(); + } + + @Override + public void logout() throws LoginException { + lc.logout(); + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java new file mode 100644 index 0000000..97acf66 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/ArgeoSecurityManager.java @@ -0,0 +1,159 @@ +package org.argeo.security.jackrabbit; + +import java.security.Principal; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import javax.jcr.Credentials; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +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.authentication.AuthContext; +import org.apache.jackrabbit.core.security.authentication.CallbackHandlerImpl; +import org.apache.jackrabbit.core.security.authorization.WorkspaceAccessManager; +import org.apache.jackrabbit.core.security.principal.AdminPrincipal; +import org.apache.jackrabbit.core.security.principal.PrincipalProvider; +import org.argeo.api.cms.AnonymousPrincipal; +import org.argeo.api.cms.CmsConstants; +import org.argeo.api.cms.CmsLog; +import org.argeo.api.cms.DataAdminPrincipal; + +/** Customises Jackrabbit security. */ +public class ArgeoSecurityManager extends DefaultSecurityManager { + private final static CmsLog log = CmsLog.getLog(ArgeoSecurityManager.class); + +// private BundleContext cmsBundleContext = null; + + public ArgeoSecurityManager() { +// if (FrameworkUtil.getBundle(CmsSession.class) != null) { +// cmsBundleContext = FrameworkUtil.getBundle(CmsSession.class).getBundleContext(); +// } + } + + public AuthContext getAuthContext(Credentials creds, Subject subject, String workspaceName) + throws RepositoryException { + checkInitialized(); + + CallbackHandler cbHandler = new CallbackHandlerImpl(creds, getSystemSession(), getPrincipalProviderRegistry(), + adminId, anonymousId); + String appName = "Jackrabbit"; + return new ArgeoAuthContext(appName, subject, cbHandler); + } + + @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); + } + } + + @Override + protected PrincipalProvider createDefaultPrincipalProvider(Properties[] moduleConfig) throws RepositoryException { + return super.createDefaultPrincipalProvider(moduleConfig); + } + + /** 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 userPrincipal = subject.getPrincipals(X500Principal.class); + boolean isRegularUser = !userPrincipal.isEmpty(); +// CmsSession cmsSession = null; +// if (cmsBundleContext != null) { +// cmsSession = CmsOsgiUtils.getCmsSession(cmsBundleContext, subject); +// if (log.isTraceEnabled()) +// log.trace("Opening JCR session for CMS session " + cmsSession); +// } + + if (isAnonymous) { + if (isDataAdmin || isJackrabbitSystem || isRegularUser) + throw new IllegalStateException("Inconsistent " + subject); + else + return CmsConstants.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 CmsConstants.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(); + ArgeoWorkspaceAccessManagerImpl workspaceAccessManager = new ArgeoWorkspaceAccessManagerImpl(wam); + if (log.isTraceEnabled()) + log.trace("Created workspace access manager"); + return workspaceAccessManager; + } + + 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); + Repository repository = systemSession.getRepository(); + if (log.isTraceEnabled()) + log.trace("Initialised workspace access manager on repository " + repository + + ", systemSession workspace: " + systemSession.getWorkspace().getName()); + } + + public void close() throws RepositoryException { + } + + public boolean grants(Set principals, String workspaceName) throws RepositoryException { + // TODO: implements finer access to workspaces + if (log.isTraceEnabled()) + log.trace("Grants " + new HashSet<>(principals) + " access to workspace '" + workspaceName + "'"); + return true; + // return wam.grants(principals, workspaceName); + } + } + +} diff --git a/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/SystemJackrabbitLoginModule.java b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/SystemJackrabbitLoginModule.java new file mode 100644 index 0000000..0f63957 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/SystemJackrabbitLoginModule.java @@ -0,0 +1,67 @@ +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.api.cms.DataAdminPrincipal; + +/** JAAS login module used when initiating a new Jackrabbit session. */ +public class SystemJackrabbitLoginModule implements LoginModule { + private Subject subject; + + @Override + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, + Map options) { + this.subject = subject; + } + + @Override + public boolean login() throws LoginException { + return true; + } + + @Override + public boolean commit() throws LoginException { + Set anonPrincipal = subject + .getPrincipals(org.argeo.api.cms.AnonymousPrincipal.class); + if (!anonPrincipal.isEmpty()) { + subject.getPrincipals().add(new AnonymousPrincipal()); + return true; + } + + Set initPrincipal = subject.getPrincipals(DataAdminPrincipal.class); + if (!initPrincipal.isEmpty()) { + subject.getPrincipals().add(new AdminPrincipal(SecurityConstants.ADMIN_ID)); + return true; + } + + Set 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.cms.jcr/src/org/argeo/security/jackrabbit/package-info.java b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/package-info.java new file mode 100644 index 0000000..8529cc2 --- /dev/null +++ b/org.argeo.cms.jcr/src/org/argeo/security/jackrabbit/package-info.java @@ -0,0 +1,2 @@ +/** Integration of Jackrabbit with Argeo security model. */ +package org.argeo.security.jackrabbit; \ No newline at end of file diff --git a/sdk/argeo-build b/sdk/argeo-build new file mode 160000 index 0000000..872f631 --- /dev/null +++ b/sdk/argeo-build @@ -0,0 +1 @@ +Subproject commit 872f63170bb639abab6e64d6e3d6a13318d38e32 -- 2.30.2